commit c58db6b98b16ff4aae4039d203cfa98b6a860b71 Author: Mariano Cano Date: Tue Jul 17 16:06:17 2018 -0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b2511b1c --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Binaries for programs and plugins +/bin/step +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Others +*.swp +coverage.txt +output +vendor diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..2da2aa47 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] - DATE +### Added +- Initial version of `step` diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md new file mode 100644 index 00000000..5e831f99 --- /dev/null +++ b/GETTING_STARTED.md @@ -0,0 +1,44 @@ +# Getting Started with Development + +To get started with local development, you will need three things: + +- Golang installed locally (instructions available [here](https://golang.org/doc/install)) +- The repository checked out in the appropriate location of your `$GOPATH` +- A version of `make` available for usage of the `Makefile` + +Ensure you've checked out the repository into the appropriate path inside your +`$GOPATH`. For example, if your `$GOPATH` is set to `~/code`, then you'd check +this repository out at `~/code/src/github.com/smallstep/cli`. You can +learn more about `$GOPATH` in the [documentation](https://golang.org/doc/code.html#GOPATH). + +### Installing Dependencies and Bootstrapping + +Once you've cloned the repository to the appropriate location, you will now be +able to install any other dependencies via the `make bootstrap` command. + +You should only ever need to run this command once, as it will ensure you have +the right version of `dep` and `gometalinter` installed. + +### Building step + +To build step, simply run `make build` which will build the cli and place the +binary in the `bin` folder. + +### Running Tests and Linting + +Now that you've installed any dependencies, you can run the tests and lint the +code base simply by running `make`. + +If you wish to only test or lint you can run `make test` or `make lint` +respectively. + +### Adding and Removing Dependencies + +To add any dependency to the repository, simply import it into your code and +then run `dep ensure` which will update the `Gopkg.lock` file. A specific +version of a dependency can be specified by adding it to the `Gopkg.toml` file +and running `dep ensure`. + +To remove a dependency, simply remove it from the codebase and any mention of +it in the `Gopkg.toml` file and run `dep ensure` which will remove it from the +`vendor` folder while updating the `Gopkg.lock` file. diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 00000000..4fa93a11 --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,311 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + branch = "master" + name = "github.com/ThomasRooney/gexpect" + packages = ["."] + revision = "5482f03509440585d13d8f648989e05903001842" + +[[projects]] + name = "github.com/alecthomas/gometalinter" + packages = ["."] + revision = "bae2f1293d092fd8167939d5108d1b025eaef9de" + +[[projects]] + branch = "master" + name = "github.com/alecthomas/units" + packages = ["."] + revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a" + +[[projects]] + name = "github.com/asaskevich/govalidator" + packages = ["."] + revision = "ccb8e960c48f04d6935e72476ae4a51028f9e22f" + version = "v9" + +[[projects]] + name = "github.com/boombuler/barcode" + packages = [ + ".", + "qr", + "utils" + ] + revision = "3cfea5ab600ae37946be2b763b8ec2c1cf2d272d" + version = "v1.0.0" + +[[projects]] + branch = "master" + name = "github.com/bouk/monkey" + packages = ["."] + revision = "5df1f207ff77e025801505ae4d903133a0b4353f" + +[[projects]] + name = "github.com/client9/misspell" + packages = [ + ".", + "cmd/misspell" + ] + revision = "b90dc15cfd220ecf8bbc9043ecb928cef381f011" + version = "v0.3.4" + +[[projects]] + branch = "master" + name = "github.com/corpix/uarand" + packages = ["."] + revision = "2b8494104d86337cdd41d0a49cbed8e4583c0ab4" + +[[projects]] + branch = "master" + name = "github.com/golang/lint" + packages = ["golint"] + revision = "06c8688daad7faa9da5a0c2f163a3d14aac986ca" + +[[projects]] + name = "github.com/google/go-cmp" + packages = [ + "cmp", + "cmp/cmpopts", + "cmp/internal/diff", + "cmp/internal/function", + "cmp/internal/value" + ] + revision = "3af367b6b30c263d47e8895973edcca9a49cf029" + version = "v0.2.0" + +[[projects]] + branch = "master" + name = "github.com/google/shlex" + packages = ["."] + revision = "6f45313302b9c56850fc17f99e40caebce98c716" + +[[projects]] + branch = "master" + name = "github.com/gordonklaus/ineffassign" + packages = ["."] + revision = "7bae11eba15a3285c75e388f77eb6357a2d73ee2" + +[[projects]] + branch = "master" + name = "github.com/grantae/certinfo" + packages = ["."] + revision = "59d56a35515b3ab2326749924739cfe58facc991" + +[[projects]] + branch = "master" + name = "github.com/icrowley/fake" + packages = ["."] + revision = "4178557ae428460c3780a381c824a1f3aceb6325" + +[[projects]] + branch = "master" + name = "github.com/kballard/go-shellquote" + packages = ["."] + revision = "95032a82bc518f77982ea72343cc1ade730072f0" + +[[projects]] + name = "github.com/kr/pty" + packages = ["."] + revision = "fa756f09eeb418bf1cc6268c66ceaad9bb98f598" + version = "v1.1.2" + +[[projects]] + name = "github.com/nicksnyder/go-i18n" + packages = [ + "i18n", + "i18n/bundle", + "i18n/language", + "i18n/translation" + ] + revision = "0dc1626d56435e9d605a29875701721c54bc9bbd" + version = "v1.10.0" + +[[projects]] + name = "github.com/pelletier/go-toml" + packages = ["."] + revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194" + version = "v1.2.0" + +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + +[[projects]] + name = "github.com/pquerna/otp" + packages = [ + ".", + "hotp", + "totp" + ] + revision = "b7b89250c468c06871d3837bee02e2d5c155ae19" + version = "v1.0.0" + +[[projects]] + branch = "master" + name = "github.com/samfoo/ansi" + packages = ["."] + revision = "b6bd2ded7189ce35bc02233b554eb56a5146af73" + +[[projects]] + branch = "master" + name = "github.com/shurcooL/sanitized_anchor_name" + packages = ["."] + revision = "86672fcb3f950f35f2e675df2240550f2a50762f" + +[[projects]] + branch = "master" + name = "github.com/smallstep/assert" + packages = ["."] + revision = "56bdbac904282a87cfca8f33ea25a6486419e64b" + +[[projects]] + branch = "master" + name = "github.com/smallstep/go-makefile" + packages = ["."] + revision = "c6025f797567554133ce98a3fcc224b3691a9f05" + +[[projects]] + branch = "master" + name = "github.com/tsenart/deadcode" + packages = ["."] + revision = "210d2dc333e90c7e3eedf4f2242507a8e83ed4ab" + +[[projects]] + branch = "master" + name = "github.com/urfave/cli" + packages = ["."] + revision = "8e01ec4cd3e2d84ab2fe90d8210528ffbb06d8ff" + +[[projects]] + name = "github.com/weppos/publicsuffix-go" + packages = ["publicsuffix"] + revision = "386050f8211b04c965721c3591e7d96650a1ea86" + version = "v0.4.0" + +[[projects]] + branch = "master" + name = "github.com/zmap/zcrypto" + packages = [ + "json", + "x509", + "x509/ct", + "x509/pkix" + ] + revision = "8a129f796df4f732131f4617e81a0ea3c9dc63cf" + +[[projects]] + branch = "master" + name = "github.com/zmap/zlint" + packages = [ + ".", + "lints", + "util" + ] + revision = "12b8dc0338e6261fb4ad6a623c0a4c1bc99b3dfe" + +[[projects]] + branch = "master" + name = "golang.org/x/crypto" + packages = [ + "bcrypt", + "blowfish", + "curve25519", + "ed25519", + "ed25519/internal/edwards25519", + "internal/subtle", + "nacl/auth", + "nacl/box", + "nacl/secretbox", + "nacl/sign", + "pbkdf2", + "poly1305", + "salsa20/salsa", + "scrypt", + "ssh/terminal" + ] + revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602" + +[[projects]] + branch = "master" + name = "golang.org/x/lint" + packages = ["."] + revision = "06c8688daad7faa9da5a0c2f163a3d14aac986ca" + +[[projects]] + branch = "master" + name = "golang.org/x/net" + packages = ["idna"] + revision = "d0887baf81f4598189d4e12a37c6da86f0bba4d0" + +[[projects]] + branch = "master" + name = "golang.org/x/sys" + packages = [ + "unix", + "windows" + ] + revision = "ac767d655b305d4e9612f5f6e33120b9176c4ad4" + +[[projects]] + name = "golang.org/x/text" + packages = [ + "collate", + "collate/build", + "internal/colltab", + "internal/gen", + "internal/tag", + "internal/triegen", + "internal/ucd", + "language", + "secure/bidirule", + "transform", + "unicode/bidi", + "unicode/cldr", + "unicode/norm", + "unicode/rangetable" + ] + revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" + version = "v0.3.0" + +[[projects]] + branch = "master" + name = "golang.org/x/tools" + packages = [ + "go/ast/astutil", + "go/gcexportdata", + "go/internal/gcimporter", + "go/types/typeutil" + ] + revision = "57f659e14dda699044a713b0f8d1d3ff033e0455" + +[[projects]] + name = "gopkg.in/alecthomas/kingpin.v3-unstable" + packages = ["."] + revision = "63abe20a23e29e80bbef8089bd3dee3ac25e5306" + +[[projects]] + branch = "v2" + name = "gopkg.in/square/go-jose.v2" + packages = [ + ".", + "cipher", + "json", + "jwt" + ] + revision = "dac1c778d6ac5c3ef8150b3e331ed944179f56e9" + source = "github.com/maraino/go-jose" + +[[projects]] + name = "gopkg.in/yaml.v2" + packages = ["."] + revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" + version = "v2.2.1" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "173e6eb13528165fee3e441bf795759815662b55ce4f0a05d6ce91844ce81507" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 00000000..e9db25b9 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,84 @@ +# Gopkg.toml example +# +# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true +required = [ + "github.com/alecthomas/gometalinter", + "github.com/golang/lint/golint", + "github.com/client9/misspell/cmd/misspell", + "github.com/gordonklaus/ineffassign", + "github.com/tsenart/deadcode", + "github.com/smallstep/go-makefile" +] + +[[constraint]] + name = "github.com/alecthomas/gometalinter" + revision = "bae2f1293d092fd8167939d5108d1b025eaef9de" + +[[override]] + name = "gopkg.in/alecthomas/kingpin.v3-unstable" + revision = "63abe20a23e29e80bbef8089bd3dee3ac25e5306" + +[[constraint]] + name = "github.com/google/go-cmp" + version = "0.2.0" + +[[constraint]] + name = "github.com/pkg/errors" + version = "0.8.0" + +[[constraint]] + branch = "master" + name = "golang.org/x/crypto" + +[prune] + go-tests = true + unused-packages = true + +[[constraint]] + name = "gopkg.in/square/go-jose.v2" + # version = "2.1.6" + # Using special branch with pbkdf2 support + source = "github.com/maraino/go-jose" + branch = "v2" + +[[constraint]] + branch = "master" + name = "github.com/urfave/cli" + +[[constraint]] + branch = "master" + name = "github.com/grantae/certinfo" + +[[constraint]] + branch = "master" + name = "github.com/zmap/zcrypto" + +[[constraint]] + branch = "master" + name = "github.com/zmap/zlint" + +[[constraint]] + branch = "master" + name = "github.com/smallstep/assert" diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..7702f5f2 --- /dev/null +++ b/Makefile @@ -0,0 +1,131 @@ +PKG=github.com/smallstep/cli/cmd/step +BINNAME=step + +# Set V to 1 for verbose output from the Makefile +Q=$(if $V,,@) +SRC = $(shell find . -type f -name '*.go' -not -path "./vendor/*") +PREFIX?= +GOOS_OVERRIDE?= + +# Set shell to bash for `echo -e` +SHELL:=/bin/bash + +all: build lint test + +.PHONY: all + +######################################### +# Bootstrapping +######################################### + +bootstrap: + $Q which dep || go get github.com/golang/dep/cmd/dep + $Q dep ensure + +vendor: Gopkg.lock + $Q dep ensure + +BOOTSTRAP=\ + github.com/golang/lint/golint \ + github.com/client9/misspell/cmd/misspell \ + github.com/gordonklaus/ineffassign \ + github.com/tsenart/deadcode \ + github.com/alecthomas/gometalinter + +define VENDOR_BIN_TMPL +vendor/bin/$(notdir $(1)): vendor + $Q go build -o $$@ ./vendor/$(1) +VENDOR_BINS += vendor/bin/$(notdir $(1)) +endef + +$(foreach pkg,$(BOOTSTRAP),$(eval $(call VENDOR_BIN_TMPL,$(pkg)))) + +.PHONY: bootstrap vendor + +######################################### +# Build +######################################### + +# Version flags to embed in the binaries +# VERSION := $(shell [ -d .git ] && git describe --tags --always --dirty="-dev") +DATE := $(shell date -u '+%Y-%m-%d %H:%M UTC') +LDFLAGS := -ldflags='-w -X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"' +GOFLAGS := CGO_ENABLED=0 + +build: $(PREFIX)bin/$(BINNAME) + @echo "Build Complete!" + +$(PREFIX)bin/$(BINNAME): vendor $(call rwildcard,*.go) + $Q mkdir -p $(@D) + $Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -i -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG) + +.PHONY: build + +######################################### +# Go generate +######################################### + +generate: + $Q go generate ./... + +.PHONY: generate + +######################################### +# Test +######################################### +test: + $Q $(GOFLAGS) go test -short -cover ./... + +vtest: + $(Q)for d in $$(go list ./... | grep -v vendor); do \ + echo -e "TESTS FOR: for \033[0;35m$$d\033[0m"; \ + $(GOFLAGS) go test -v -bench=. -run=. -short -coverprofile=profile.coverage.out -covermode=atomic $$d; \ + out=$$?; \ + if [[ $$out -ne 0 ]]; then ret=$$out; fi;\ + rm -f profile.coverage.out; \ + done; exit $$ret; + +.PHONY: test vtest + +integration: $(PREFIX)bin/$(BINNAME) + $Q $(GOFLAGS) go test -tags=integration ./integration/... + +.PHONY: integration + +######################################### +# Linting +######################################### + +LINTERS=\ + gofmt \ + golint \ + vet \ + misspell \ + ineffassign \ + deadcode + +$(patsubst %,%-bin,$(filter-out gofmt vet,$(LINTERS))): %-bin: vendor/bin/% +gofmt-bin vet-bin: + +$(LINTERS): %: vendor/bin/gometalinter %-bin vendor + $Q PATH=`pwd`/vendor/bin:$$PATH gometalinter --tests --disable-all --vendor \ + --deadline=5m -s data -s pkg --enable $@ ./... +fmt: + $Q gofmt -l -w $(SRC) + +lint: $(LINTERS) + +.PHONY: $(LINTERS) lint fmt + +######################################### +# Clean +######################################### + +clean: + @echo "You will need to run 'make bootstrap' or 'dep ensure' directly to re-download any dependencies." + $Q rm -rf vendor +ifneq ($(BINNAME),"") + $Q rm -f bin/$(BINNAME) +endif + +.PHOMY: clean diff --git a/README.md b/README.md new file mode 100644 index 00000000..67b7a3e4 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Step CLI + +This repository contains the code for the `step` command line tool. + +TODO: add description + +Please ensure to read the [CLI Style Guide](https://github.com/urfave/cli) +before implementing any features or modifying behavior as it contains +expectations surrounding how the CLI should behave. + +All changes to behavior *must* be documented in the [CHANGELOG.md](./CHANGELOG.md). + +### Table of Contents + +- [CHANGELOG](./CHANGELOG.md) +- [Getting Started with Development](./GETTING_STARTED.md) +- [How to add a new Command](./command/README.md) diff --git a/cmd/step/main.go b/cmd/step/main.go new file mode 100644 index 00000000..cf59fbb8 --- /dev/null +++ b/cmd/step/main.go @@ -0,0 +1,180 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "log" + "net/http" + "os" + "reflect" + "regexp" + "strings" + + "github.com/urfave/cli" + + "github.com/smallstep/cli/command" + "github.com/smallstep/cli/command/version" + "github.com/smallstep/cli/config" + "github.com/smallstep/cli/usage" + + // Enabled commands + _ "github.com/smallstep/cli/command/certificates" + _ "github.com/smallstep/cli/command/crypto" + _ "github.com/smallstep/cli/command/oauth" + + // Profiling and debugging + _ "net/http/pprof" +) + +// Version is set by an LDFLAG at build time representing the git tag or commit +// for the current release +var Version = "N/A" + +// BuildTime is set by an LDFLAG at build time representing the timestamp at +// the time of build +var BuildTime = "N/A" + +func init() { + config.Set(Version, BuildTime) +} + +func main() { + // Override global framework components + cli.VersionPrinter = func(c *cli.Context) { + version.Command(c) + } + cli.AppHelpTemplate = usage.AppHelpTemplate + cli.SubcommandHelpTemplate = usage.SubcommandHelpTemplate + cli.CommandHelpTemplate = usage.CommandHelpTemplate + cli.HelpPrinter = helpPrinter + cli.FlagNamePrefixer = usage.FlagNamePrefixer + cli.FlagStringer = stringifyFlag + + // Configure cli app + app := cli.NewApp() + app.Name = "step" + app.HelpName = "step" + app.Usage = "plumbing for distributed systems" + app.Version = config.Version() + app.Commands = command.Retrieve() + app.Flags = append(app.Flags, cli.HelpFlag) + app.EnableBashCompletion = true + app.Copyright = "(c) 2018 Smallstep Inc." + + // All non-successful output should be written to stderr + app.Writer = os.Stderr + app.ErrWriter = os.Stderr + + // Start the golang debug logger if environment variable is set. + // See https://golang.org/pkg/net/http/pprof/ + debugProfAddr := os.Getenv("STEP_PROF_ADDR") + if debugProfAddr != "" { + go func() { + log.Println(http.ListenAndServe(debugProfAddr, nil)) + }() + } + + if err := app.Run(os.Args); err != nil { + if os.Getenv("STEPDEBUG") == "1" { + fmt.Fprintf(os.Stderr, "%+v\n", err) + } else { + fmt.Fprintln(os.Stderr, err) + } + os.Exit(1) + } +} + +var sectionRe = regexp.MustCompile(`(?m:^##)`) + +//var sectionRe = regexp.MustCompile(`^## [^\n]*$`) + +func findSectionEnd(h, s string) int { + start := strings.Index(s, fmt.Sprintf("## %s", h)) + if start == -1 { + return start + } + nextSection := sectionRe.FindStringIndex(s[start+2:]) + if nextSection == nil { + return len(s) + } + return start + 2 + nextSection[0] +} + +// Convert some stuff that we can't easily write in help files because +// backticks and raw strings don't mix: +// - "" to "`foo`" +// - "'''" to "```" +func markdownify(b []byte) []byte { + for i := 0; i < len(b); i++ { + switch b[i] { + case '<': + if b[i-1] != '\\' { + b[i] = '`' + } + case '>': + if b[i-1] != '\\' { + b[i] = '`' + } + case '\'': + if len(b) >= i+3 && string(b[i:i+3]) == "'''" { + b[i] = '`' + b[i+1] = '`' + b[i+2] = '`' + i += 2 + } + } + } + return b +} + +func helpPrinter(w io.Writer, templ string, data interface{}) { + buf := new(bytes.Buffer) + cli.HelpPrinterCustom(buf, templ, data, nil) + //w.Write(buf.Bytes()) + s := string(markdownify(buf.Bytes())) + // Move the OPTIONS section to the right place. urfave puts them at the end + // of the file, we want them to be after POSITIONAL ARGUMENTS, DESCRIPTION, + // USAGE, or NAME (in that order, depending on which sections exist). + optLoc := strings.Index(s, "## OPTIONS") + if optLoc != -1 { + optEnd := findSectionEnd("OPTIONS", s) + if optEnd != -1 { + options := s[optLoc:optEnd] + s = s[:optLoc] + s[optEnd:] + if newLoc := findSectionEnd("POSITIONAL ARGUMENTS", s); newLoc != -1 { + s = s[:newLoc] + options + s[newLoc:] + } else if newLoc := findSectionEnd("DESCRIPTION", s); newLoc != -1 { + s = s[:newLoc] + options + s[newLoc:] + } else if newLoc := findSectionEnd("USAGE", s); newLoc != -1 { + s = s[:newLoc] + options + s[newLoc:] + } else if newLoc := findSectionEnd("NAME", s); newLoc != -1 { + s = s[:newLoc] + options + s[newLoc:] + } else { + // Keep it at the end I guess :/. + s = s + options + } + } + } + w.Write(usage.Render([]byte(s))) +} + +func flagValue(f cli.Flag) reflect.Value { + fv := reflect.ValueOf(f) + for fv.Kind() == reflect.Ptr { + fv = reflect.Indirect(fv) + } + return fv +} + +var placeholderString = regexp.MustCompile(`<.*?>`) + +func stringifyFlag(f cli.Flag) string { + fv := flagValue(f) + usage := fv.FieldByName("Usage").String() + placeholder := placeholderString.FindString(usage) + if placeholder == "" { + placeholder = "" + } + return cli.FlagNamePrefixer(fv.FieldByName("Name").String(), placeholder) + "\t" + usage +} diff --git a/command/README.md b/command/README.md new file mode 100644 index 00000000..7dcdd7d4 --- /dev/null +++ b/command/README.md @@ -0,0 +1,104 @@ +# How to add a new Command + +Before making any changes, please consult the [CLI style guide](https://github.com/urfave/cli)! + +### Package Layout + +The [`urfave/cli`](https://github.com/urfave/cli) package that forms the basis +of Step CLI supports N-levels of command hierarchy. Each level of the hierarchy +should exist within its own package if possible. For example, `version` and +`help` exist inside their own packages inside the top-level `command` package. + +Any package used by a command but does not contain explicit business logic +directly related to the command should exist in the top-level of this +repository. For example, the `github.com/smallstep/cli/flags` and +`github.com/smallstep/cli/errs` package are used by many different +commands and contain functionality for defining flags and creating/manipulating +errors. + +### Adding a Command + +Once you figured out where to add the command inside the package hierarchy you +must register the command. This way the command *can* be made available if +desired inside the `cmd/step/main.go`. + +An example of defining a command and registering it: + +```golang +package validate + +import ( + "github.com/urfave/cli" + + "github.com/smallstep/cli/command" + "github.com/smallstep/cli/flags" +) + +func init() { + cmd := cli.Command{ + Name: "validate", + Usage: "Returns whether or not the provided token is valid", + Flags: []cli.Flag{ + flags.Token("The one-time token value to validate"), + }, + Action: validate, + } + + command.Register(validate) +} +``` + +Once this is done, you then must import the pkg inside `cmd/step/main.go` so +the packages `init` method is run appropriately. This only needs to be done for +top-level commands. + +```golang +package main + +import ( + "github.com/urfave/cli" + + _ "github.com/smallstep/cli/validate" +) +``` + +This will ensure that the `smallstep/cli/validate` package is initialized +and thus registered with the `smallstep/cli/command`. + +### Usage, Flags, Errors, and Prompts + +There are three packages which contain functionality to make writing commands easier: + +- `github.com/smallstep/cli/usage` +- `github.com/smallstep/cli/flags` +- `github.com/smallstep/cli/prompts` +- `github.com/smallstep/cli/errs` + +The usage package is used to extend the default documentation provided by +`urfave/cli` by enabling us to document arguments, whether they are optional or +required, and ensuring they're printed out as a part of the `step help` or +`step -h` flow. If you need to add a different type of annotation to +document an argument just add it to the `usage.Argument` struct! + +When you add a flag, look into the pre-existing ones inside the `flags` +package. Could you use one of the pre-existing flags in order to reduce +duplication? If not, make sure to add a flag so it could be used in future! + +The `errs` package contains functionality for defining and working with errors +to ensure they are mutated properly into a `urfave/cli.ExitError` which ensures +the process returns an appropriate exit code on termination. When you create an +error, consider whether or not it's general and could be predefined inside the +`errs` package. Errors that are specific to the command itself should exist +only inside that commands respective package. + +The `prompts` package is a small wrapper around the various different types of +prompts used by the commands. If you need a new prompt, consider adding a new +function to this package to tailor the prompt for the step cli. This way other +commands can adopt the step aesthetic as new functionality is introduced. + +### Hiding a Command + +Sometimes it's desirable to prevent a command from showing up in the help menu +because it's been deprecated *or* it's not ready for users to leverage. This +can be achieved by setting the `Hidden` property on the `cli.Command` struct to +`true`. diff --git a/command/certificates/bundle.go b/command/certificates/bundle.go new file mode 100644 index 00000000..188186fb --- /dev/null +++ b/command/certificates/bundle.go @@ -0,0 +1,65 @@ +package certificates + +import ( + "encoding/pem" + "io/ioutil" + + "github.com/pkg/errors" + "github.com/smallstep/cli/errs" + "github.com/urfave/cli" +) + +func bundleCommand() cli.Command { + return cli.Command{ + Name: "bundle", + Action: cli.ActionFunc(bundleAction), + Usage: `bundle a certificate with intermediate certificate(s) needed for certificate path validation.`, + UsageText: `step certificates bundle CRT_FILE BUNDLE_FILE`, + Description: `The 'step certificates bundle' command bundles a certificate with any +intermediates necessary to validate the certificate. + + POSITIONAL ARGUMENTS + CRT_FILE + The path to a leaf certificate to bundle with issuing certificate(s). + + CA_FILE + The path to the Certificate Authoriy issusing certificate for the leaf. + + BUNDLE_FILE + The path to write the bundle.`, + } +} + +func bundleAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 3); err != nil { + return err + } + + crtFile := ctx.Args().Get(0) + crtBytes, err := ioutil.ReadFile(crtFile) + if err != nil { + return errs.FileError(err, crtFile) + } + crtBlock, _ := pem.Decode(crtBytes) + if crtBlock == nil { + return errors.Errorf("could not parse certificate file '%s'", crtFile) + } + + caFile := ctx.Args().Get(1) + caBytes, err := ioutil.ReadFile(caFile) + if err != nil { + return errs.FileError(err, caFile) + } + caBlock, _ := pem.Decode(caBytes) + if caBlock == nil { + return errors.Errorf("could not parse certificate file '%s'", caFile) + } + + chainFile := ctx.Args().Get(2) + if err := ioutil.WriteFile(chainFile, + append(pem.EncodeToMemory(crtBlock), pem.EncodeToMemory(caBlock)...), 0600); err != nil { + return errs.FileError(err, chainFile) + } + + return nil +} diff --git a/command/certificates/certificates.go b/command/certificates/certificates.go new file mode 100644 index 00000000..da99bad0 --- /dev/null +++ b/command/certificates/certificates.go @@ -0,0 +1,37 @@ +package certificates + +import ( + "github.com/smallstep/cli/command" + "github.com/urfave/cli" +) + +// Command returns the cli.Command for jwt and related subcommands. +func init() { + cmd := cli.Command{ + Name: "certificates", + Usage: "create, revoke, validate, bundle, and otherwise manage certificates.", + UsageText: "step certificates [arguments] [global-flags] [subcommand-flags]", + Description: `The 'step certificates' command group provides facilities for creating + certificate signing requests (CSRs), creating self-signed certificates + (e.g., for use as a root certificate authority), generating leaf or + intermediate CA certificate by signing a CSR, validating certificates, + renewing certificates, generating certificate bundles, and key-wrapping + of private keys. + + More information about certificates in general (as opposed to the + certificates commands) can be found at 'step help topics certificates' + or online at [URL].`, + + Subcommands: cli.Commands{ + bundleCommand(), + createCommand(), + inspectCommand(), + lintCommand(), + //renewCommand(), + signCommand(), + verifyCommand(), + }, + } + + command.Register(cmd) +} diff --git a/command/certificates/create.go b/command/certificates/create.go new file mode 100644 index 00000000..075939bf --- /dev/null +++ b/command/certificates/create.go @@ -0,0 +1,254 @@ +package certificates + +import ( + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/pkg/errors" + stepx509 "github.com/smallstep/cli/crypto/certificates/x509" + "github.com/smallstep/cli/crypto/keys" + "github.com/smallstep/cli/errs" + "github.com/smallstep/cli/utils/reader" + "github.com/urfave/cli" +) + +func createCommand() cli.Command { + return cli.Command{ + Name: "create", + Action: cli.ActionFunc(createAction), + Usage: "create a certificate or certificate signing request", + UsageText: `step certificates create SUBJECT CRT_FILE KEY_FILE [--type=CERTIFICATE_TYPE] + [--profile=PROFILE] [--csr] [--token=TOKEN]`, + Description: `The 'step certificates create' command generates a certificate or a + certificate signing requests (CSR) that can be signed later using 'step + certificates sign' (or some other tool) to produce a certificate. + + This command can create x.509 certificates for use with TLS as well as SSH + certificates. + +POSITIONAL ARGUMENTS + SUBJECT + The subject of the certificate. Typically this is a hostname for services or an email address for people. + + CRT_FILE + File to write CRT or CSR to (PEM format) + + KEY_FILE + File to write private key to (PEM format)`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "type", + Value: "x509", + Usage: `The type of certificate to generate. If not specified default is x509. + + CERTIFICATE_TYPE must be one of: + x509 + Generate an x.509 certificate suitable for use with TLS. + ssh + Generate an SSH certificate.`, + }, + cli.StringFlag{ + Name: "profile", + Value: "leaf", + Usage: `The certificate profile sets various certificate details such as + certificate use and expiration. The default profile is 'leaf' which is suitable + for a client or server using TLS. + + PROFILE must be one of: + leaf + Generate a leaf x.509 certificate suitable for use with TLs. + intermediate-ca + Generate a certificate that can be used to sign additional leaf or intermediate certificates. + root-ca + Generate a new self-signed root certificate suitable for use as a root CA.`, + }, + cli.StringFlag{ + Name: "token", + Usage: `A provisioning token or bootstrap token for authenticating to a remote CA.`, + }, + cli.BoolFlag{ + Name: "csr", + Usage: `Generate a certificate signing request (CSR) instead of a certificate.`, + }, + cli.StringFlag{ + Name: "ca", + Usage: `The certificate authority used to issue the new certificate (PEM file).`, + }, + cli.StringFlag{ + Name: "ca-key", + Usage: `The certificate authority private key used to sign the new certificate (PEM file).`, + }, + cli.BoolFlag{ + Name: "no-password", + Usage: "TODO, requires --insecure", + }, + cli.BoolFlag{ + Name: "subtle", + Hidden: true, + }, + cli.BoolFlag{ + Name: "insecure", + Hidden: true, + }, + }, + } +} + +func createAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 3); err != nil { + return err + } + + // Use password to protect private JWK by default + usePassword := true + if ctx.Bool("no-password") { + if ctx.Bool("insecure") { + usePassword = false + } else { + return errs.RequiredWithFlag(ctx, "insecure", "no-password") + } + } + + subject := ctx.Args().Get(0) + crtFile := ctx.Args().Get(1) + keyFile := ctx.Args().Get(2) + if crtFile == keyFile { + return errs.EqualArguments(ctx, "CRT_FILE", "KEY_FILE") + } + + typ := ctx.String("type") + prof := ctx.String("profile") + caPath := ctx.String("ca") + caKeyPath := ctx.String("ca-key") + if ctx.Bool("csr") { + typ = "x509-csr" + } + + var ( + err error + priv interface{} + pubPEM *pem.Block + ) + switch typ { + case "x509-csr": + priv, err = keys.GenerateDefaultKey() + if err != nil { + return errors.WithStack(err) + } + + _csr := &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: subject, + }, + } + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, _csr, priv) + if err != nil { + return errors.WithStack(err) + } + + pubPEM = &pem.Block{ + Type: "CSR", + Bytes: csrBytes, + Headers: map[string]string{}, + } + case "x509": + var ( + err error + profile stepx509.Profile + ) + switch prof { + case "leaf": + issIdentity, err := loadIssuerIdentity(prof, caPath, caKeyPath) + if err != nil { + return errors.WithStack(err) + } + profile, err = stepx509.NewLeafProfile(subject, issIdentity.Crt, + issIdentity.Key) + if err != nil { + return errors.WithStack(err) + } + case "intermediate-ca": + issIdentity, err := loadIssuerIdentity(prof, caPath, caKeyPath) + if err != nil { + return errors.WithStack(err) + } + if err != nil { + return errors.WithStack(err) + } + profile, err = stepx509.NewIntermediateProfile(subject, + issIdentity.Crt, issIdentity.Key) + if err != nil { + return errors.WithStack(err) + } + case "root-ca": + profile, err = stepx509.NewRootProfile(subject) + if err != nil { + return errors.WithStack(err) + } + default: + return errs.InvalidFlagValue(ctx, "profile", prof, "leaf, intermediate-ca, root-ca") + } + crtBytes, err := profile.CreateCertificate() + if err != nil { + return errors.WithStack(err) + } + pubPEM = &pem.Block{ + Type: "CERTIFICATE", + Bytes: crtBytes, + Headers: map[string]string{}, + } + priv = profile.SubjectPrivateKey() + case "ssh": + return errors.Errorf("implementation incomplete! Come back later ...") + default: + return errs.InvalidFlagValue(ctx, "type", typ, "x509, ssh") + } + + if err := ioutil.WriteFile(crtFile, pem.EncodeToMemory(pubPEM), + os.FileMode(0600)); err != nil { + return errs.FileError(err, crtFile) + } + + var pass string + if usePassword { + if err := reader.ReadPasswordSubtle( + fmt.Sprintf("Password with which to encrypt private key file `%s`: ", keyFile), + &pass, "Password", reader.RetryOnEmpty); err != nil { + return errors.WithStack(err) + } + } + if err := keys.WritePrivateKey(priv, pass, keyFile); err != nil { + return errors.WithStack(err) + } + return nil +} + +func loadIssuerIdentity(profile, caPath, caKeyPath string) (*stepx509.Identity, error) { + if caPath == "" { + return nil, errors.Errorf("Missing value for flag '--ca'.\n\nFlags "+ + "'--ca' and '--ca-key' are required when creating a %s x509 Certificate.", + strings.Title(profile)) + } + if caKeyPath == "" { + return nil, errors.Errorf("Missing value for flag '--ca-key'.\n\nFlags "+ + "'--ca' and '--ca-key' are required when creating a %s x509 Certificate.", + strings.Title(profile)) + } + return stepx509.LoadIdentityFromDisk(caPath, caKeyPath, + func() (string, error) { + var pass string + if err := reader.ReadPasswordSubtle( + fmt.Sprintf("Password with which to decrypt CA private key file `%s`: ", caKeyPath), + &pass, "Password", reader.RetryOnEmpty); err != nil { + return "", errors.WithStack(err) + } + return pass, nil + }) + +} diff --git a/command/certificates/inspect.go b/command/certificates/inspect.go new file mode 100644 index 00000000..fa808c1c --- /dev/null +++ b/command/certificates/inspect.go @@ -0,0 +1,166 @@ +package certificates + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/grantae/certinfo" + "github.com/pkg/errors" + stepx509 "github.com/smallstep/cli/crypto/certificates/x509" + "github.com/smallstep/cli/errs" + "github.com/urfave/cli" + zx509 "github.com/zmap/zcrypto/x509" +) + +func inspectCommand() cli.Command { + return cli.Command{ + Name: "inspect", + Action: cli.ActionFunc(inspectAction), + Usage: `print certificate or CSR details in human readable format.`, + UsageText: `step certificates inspect CRT_FILE [--format=FORMAT]`, + Description: `The 'step certificates inspect' command prints the details of a certificate +or CSR in a human readable format. Output from the inspect command is printed to +STDERR instead of STDOUT unless. This is an intentional barrier to accidental +misuse: scripts should never rely on the contents of an unvalidated certificate. +For scripting purposes, use 'step certificates verify'. + + POSITIONAL ARGUMENTS + CRT_FILE + The path to a certificate or certificate signing request (CSR) to inspect.`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "format", + Value: "text", + Usage: `The output format for printing the introspection details. + + FORMAT must be one of: + text + Print output in unstructured text suitable for a human to read + json + Print output in JSON format`, + }, + cli.StringFlag{ + Name: "roots", + Usage: `Root certificate(s) to use in request to obtain remote server certificate. + + ROOTS is a string containing a (FILE | LIST of FILES | DIRECTORY) defined in one of the following ways: + FILE + Relative or full path to a file. All certificates in the file will be used for path validation. + LIST of Files + Comma-separated list of relative or full file paths. Every PEM encoded certificate + from each file will be used for path validation. + DIRECTORY + Relative or full path to a directory. Every PEM encoded certificate from each file + in the directory will be used for path validation.`, + }, + }, + } +} + +func inspectAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 1); err != nil { + return err + } + + var ( + crtFile = ctx.Args().Get(0) + block *pem.Block + ) + if strings.HasPrefix(crtFile, "https://") { + var ( + err error + rootCAs *x509.CertPool + ) + if roots := ctx.String("roots"); roots != "" { + rootCAs, err = stepx509.ReadCertPool(roots) + if err != nil { + errors.Wrapf(err, "failure to load root certificate pool from input path '%s'", roots) + } + } + addr := strings.TrimPrefix(crtFile, "https://") + if !strings.Contains(addr, ":") { + addr += ":443" + } + conn, err := tls.Dial("tcp", addr, &tls.Config{RootCAs: rootCAs}) + if err != nil { + return errors.Wrapf(err, "failed to connect") + } + conn.Close() + crt := conn.ConnectionState().PeerCertificates[0] + block = &pem.Block{ + Type: "CERTIFICATE", + Bytes: crt.Raw, + } + } else { + crtBytes, err := ioutil.ReadFile(crtFile) + if err != nil { + return errs.FileError(err, crtFile) + } + block, _ = pem.Decode(crtBytes) + if block == nil { + return errors.Errorf("could not parse certificate file '%s'", crtFile) + } + } + + format := ctx.String("format") + switch block.Type { + case "CERTIFICATE": + switch format { + case "text": + crt, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return errors.WithStack(err) + } + result, err := certinfo.CertificateText(crt) + if err != nil { + return errors.WithStack(err) + } + fmt.Print(result) + case "json": + zcrt, err := zx509.ParseCertificate(block.Bytes) + if err != nil { + return errors.WithStack(err) + } + b, err := json.MarshalIndent(struct { + *zx509.Certificate + }{zcrt}, "", " ") + if err != nil { + return errors.WithStack(err) + } + os.Stdout.Write(b) + default: + return errors.Errorf("invalid value for '--format'. '--format' must be "+ + "one of 'text'(default) or 'json', but got '%s'", format) + } + case "CSR": + switch format { + case "text": + return errors.Errorf("Not implemented. Come back later :)") + case "json": + zcsr, err := zx509.ParseCertificateRequest(block.Bytes) + if err != nil { + return errors.WithStack(err) + } + b, err := json.MarshalIndent(struct { + *zx509.CertificateRequest + }{zcsr}, "", " ") + if err != nil { + return errors.WithStack(err) + } + os.Stdout.Write(b) + default: + return errors.Errorf("invalid value for '--format'. '--format' must be "+ + "one of 'text'(default) or 'json', but got '%s'", format) + } + default: + return errors.Errorf("Invalid PEM type in '%s'. Expected ['CERTIFICATE'|'CSR'] but got '%s')", crtFile, block.Type) + } + + return nil +} diff --git a/command/certificates/lint.go b/command/certificates/lint.go new file mode 100644 index 00000000..f30665cb --- /dev/null +++ b/command/certificates/lint.go @@ -0,0 +1,109 @@ +package certificates + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "encoding/pem" + "io/ioutil" + "os" + "strings" + + "github.com/pkg/errors" + stepx509 "github.com/smallstep/cli/crypto/certificates/x509" + "github.com/smallstep/cli/errs" + "github.com/urfave/cli" + zx509 "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint" +) + +func lintCommand() cli.Command { + return cli.Command{ + Name: "lint", + Action: cli.ActionFunc(lintAction), + Usage: `lint certificate details.`, + UsageText: `step certificates lint CRT_FILE`, + Description: `UPDATE ME + + POSITIONAL ARGUMENTS + CRT_FILE + The path to a certificate or certificate signing request (CSR) to inspect.`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "roots", + Usage: `Root certificate(s) to use in request to obtain remote server certificate. + + ROOTS is a string containing a (FILE | LIST of FILES | DIRECTORY) defined in one of the following ways: + FILE + Relative or full path to a file. All certificates in the file will be used for path validation. + LIST of Files + Comma-separated list of relative or full file paths. Every PEM encoded certificate + from each file will be used for path validation. + DIRECTORY + Relative or full path to a directory. Every PEM encoded certificate from each file + in the directory will be used for path validation.`, + }, + }, + } +} + +func lintAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 1); err != nil { + return err + } + + var ( + crtFile = ctx.Args().Get(0) + block *pem.Block + ) + if strings.HasPrefix(crtFile, "https://") { + var ( + err error + rootCAs *x509.CertPool + ) + if roots := ctx.String("roots"); roots != "" { + rootCAs, err = stepx509.ReadCertPool(roots) + if err != nil { + errors.Wrapf(err, "failure to load root certificate pool from input path '%s'", roots) + } + } + addr := strings.TrimPrefix(crtFile, "https://") + if !strings.Contains(addr, ":") { + addr += ":443" + } + conn, err := tls.Dial("tcp", addr, &tls.Config{RootCAs: rootCAs}) + if err != nil { + return errors.Wrapf(err, "failed to connect") + } + conn.Close() + crt := conn.ConnectionState().PeerCertificates[0] + block = &pem.Block{ + Type: "CERTIFICATE", + Bytes: crt.Raw, + } + } else { + crtBytes, err := ioutil.ReadFile(crtFile) + if err != nil { + return errs.FileError(err, crtFile) + } + block, _ = pem.Decode(crtBytes) + if block == nil { + return errors.Errorf("could not parse certificate file '%s'", crtFile) + } + } + + zcrt, err := zx509.ParseCertificate(block.Bytes) + if err != nil { + return errors.WithStack(err) + } + zlintResult := zlint.LintCertificate(zcrt) + b, err := json.MarshalIndent(struct { + *zlint.ResultSet + }{zlintResult}, "", " ") + if err != nil { + return errors.WithStack(err) + } + os.Stdout.Write(b) + + return nil +} diff --git a/command/certificates/sign.go b/command/certificates/sign.go new file mode 100644 index 00000000..d020342e --- /dev/null +++ b/command/certificates/sign.go @@ -0,0 +1,101 @@ +package certificates + +import ( + "encoding/pem" + "fmt" + "io/ioutil" + + "github.com/pkg/errors" + stepx509 "github.com/smallstep/cli/crypto/certificates/x509" + "github.com/smallstep/cli/crypto/keys" + "github.com/smallstep/cli/errs" + "github.com/smallstep/cli/utils/reader" + "github.com/urfave/cli" +) + +func signCommand() cli.Command { + return cli.Command{ + Name: "sign", + Action: cli.ActionFunc(signAction), + Usage: "sign a certificate signing request (CSR).", + UsageText: `step certificates sign CSR_FILE CRT_FILE KEY_FILE [--token=TOKEN]`, + Description: `The 'step certificates sign' generates a signed certificate from a + certificate signing requests (CSR). + + POSITIONAL ARGUMENTS + CSR_FILE + The path to a certificate signing request (CSR) to be signed. + CRT_FILE + The path to an issuing certificate. + KEY_FILE + The path to a private key for signing the CSR.`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "token", + Usage: `A provisioning token or bootstrap token for secure introduction and + mutual authentication with an unknown CA.`, + }, + }, + } +} + +func signAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 3); err != nil { + return err + } + + csrFile := ctx.Args().Get(0) + crtFile := ctx.Args().Get(1) + keyFile := ctx.Args().Get(2) + + // Load the CSR into an x509 Certificate Template. + csrBytes, err := ioutil.ReadFile(csrFile) + if err != nil { + return errors.WithStack(err) + } + csr, err := stepx509.LoadCSRFromBytes(csrBytes) + if err != nil { + return errors.WithStack(err) + } + // Load the Issuer Certificate. + issuerCrt, _, err := stepx509.LoadCertificate(crtFile) + if err != nil { + return errors.WithStack(err) + } + + // Load the Issuer Private Key. + keyBytes, err := ioutil.ReadFile(keyFile) + if err != nil { + return errors.WithStack(err) + } + key, err := keys.LoadPrivateKey(keyBytes, func() (string, error) { + var pass string + if err := reader.ReadPasswordSubtle( + fmt.Sprintf("Password with which to decrypt private key %s: ", keyFile), + &pass, "Password", reader.RetryOnEmpty); err != nil { + return "", err + } + return pass, nil + }) + if err != nil { + return errors.WithStack(err) + } + + leafProfile, err := stepx509.NewLeafProfileWithCSR(csr, issuerCrt, key) + if err != nil { + return errors.WithStack(err) + } + + crtBytes, err := leafProfile.CreateCertificate() + if err != nil { + return errors.Wrapf(err, "failure creating new leaf certificate from input csr") + } + block := &pem.Block{ + Type: "CERTIFICATE", + Bytes: crtBytes, + } + fmt.Printf("%s", string(pem.EncodeToMemory(block))) + + //tok := ctx.String("token") + return nil +} diff --git a/command/certificates/verify.go b/command/certificates/verify.go new file mode 100644 index 00000000..279269a0 --- /dev/null +++ b/command/certificates/verify.go @@ -0,0 +1,116 @@ +package certificates + +import ( + "crypto/x509" + "encoding/pem" + "io/ioutil" + + "github.com/pkg/errors" + stepx509 "github.com/smallstep/cli/crypto/certificates/x509" + "github.com/smallstep/cli/errs" + "github.com/urfave/cli" +) + +func verifyCommand() cli.Command { + return cli.Command{ + Name: "verify", + Action: cli.ActionFunc(verifyAction), + Usage: `verify a certificate.`, + UsageText: `step certificates verify CRT_FILE [--host=HOST]`, + Description: `The 'step certificates verify' command executes the certificate path validation +algorithm for x.509 certificates defined in RFC 5280. If the certificate is valid +this command will return '0'. If validation fails, or if an error occurs, this +command will produce a non-zero return value. + + POSITIONAL ARGUMENTS + CRT_FILE + The path to a certificate to validate.`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "host", + Usage: `Check whether the certificate is for the specified host.`, + }, + cli.StringFlag{ + Name: "roots", + Usage: `Root certificates to use in the path validation algorithm. + + ROOTS is a string containing a (FILE | LIST of FILES | DIRECTORY) defined in one of the following ways: + FILE + Relative or full path to a file. All certificates in the file will be used for path validation. + LIST of Files + Comma-separated list of relative or full file paths. Every PEM encoded certificate + from each file will be used for path validation. + DIRECTORY + Relative or full path to a directory. Every PEM encoded certificate from each file + in the directory will be used for path validation.`, + }, + }, + } +} + +func verifyAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 1); err != nil { + return err + } + + crtFile := ctx.Args().Get(0) + crtBytes, err := ioutil.ReadFile(crtFile) + if err != nil { + return errs.FileError(err, crtFile) + } + + var ( + crt *x509.Certificate + ipems []byte + intermediatePool = x509.NewCertPool() + block *pem.Block + ) + // The first certificate PEM in the file is our leaf Certificate. + // Any certificate after the first is added to the list of Intermediate + // certificates used for path validation. + for len(crtBytes) > 0 { + block, crtBytes = pem.Decode(crtBytes) + if block == nil { + break + } + if block.Type != "CERTIFICATE" { + continue + } + if crt == nil { + crt, err = x509.ParseCertificate(block.Bytes) + if err != nil { + return errors.WithStack(err) + } + } else { + ipems = append(ipems, pem.EncodeToMemory(block)...) + } + } + if len(ipems) > 0 && !intermediatePool.AppendCertsFromPEM(ipems) { + return errors.Errorf("failure creating intermediate list from certificate '%s'", crtFile) + } + + var ( + host = ctx.String("host") + roots = ctx.String("roots") + rootPool *x509.CertPool + ) + + if roots != "" { + rootPool, err = stepx509.ReadCertPool(roots) + if err != nil { + errors.Wrapf(err, "failure to load root certificate pool from input path '%s'", roots) + } + } + + opts := x509.VerifyOptions{ + DNSName: host, + Roots: rootPool, + Intermediates: intermediatePool, + } + + if _, err := crt.Verify(opts); err != nil { + return errors.Wrapf(err, "failed to verify certificate") + } + + return nil +} diff --git a/command/command.go b/command/command.go new file mode 100644 index 00000000..2d586114 --- /dev/null +++ b/command/command.go @@ -0,0 +1,161 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/urfave/cli" +) + +var cmds []cli.Command + +var helpCommand cli.Command + +func init() { + helpCommand = createHelpCommand() + cmds = []cli.Command{ + helpCommand, + } +} + +// Register adds the given command to the global list of commands +func Register(c cli.Command) { + cmds = append(cmds, c) +} + +// Retrieve returns all commands +func Retrieve() []cli.Command { + return cmds +} + +// helpCommand overwrites default urfvafe/cli help command to support one or +// multiple subcommands like: +// step help +// step help crypto +// step help crypto jwt +// step help crypto jwt sign +// ... +func createHelpCommand() cli.Command { + return cli.Command{ + Name: "help", + Aliases: []string{"h"}, + Usage: "displays help for the specified command or command group", + ArgsUsage: "[command]", + Action: cli.ActionFunc(func(ctx *cli.Context) error { + args := ctx.Args() + if args.Present() { + last := len(args) - 1 + lastName := args[last] + subcmd := ctx.App.Commands + parent := createParentCommand(ctx) + + for _, name := range args[:last] { + for _, cmd := range subcmd { + if cmd.HasName(name) { + parent = cmd + subcmd = cmd.Subcommands + break + } + } + } + + for _, cmd := range subcmd { + if cmd.HasName(lastName) { + cmd.HelpName = fmt.Sprintf("%s %s", ctx.App.HelpName, strings.Join(args, " ")) + parent.HelpName = fmt.Sprintf("%s %s", ctx.App.HelpName, strings.Join(args[:last], " ")) + + ctx.Command = cmd + if len(cmd.Subcommands) == 0 { + ctx.App = createCliApp(ctx, parent) + return cli.ShowCommandHelp(ctx, lastName) + } + + ctx.App = createCliApp(ctx, cmd) + return cli.ShowCommandHelp(ctx, "") + } + } + + return cli.NewExitError(fmt.Sprintf("No help topic for '%s %s'", ctx.App.Name, strings.Join(args, " ")), 3) + } + + cli.ShowAppHelp(ctx) + return nil + }), + } +} + +// createParentCommand returns a command representation of the app. +func createParentCommand(ctx *cli.Context) cli.Command { + return cli.Command{ + Name: ctx.App.Name, + HelpName: ctx.App.HelpName, + Usage: ctx.App.Usage, + UsageText: ctx.App.UsageText, + ArgsUsage: ctx.App.ArgsUsage, + Description: ctx.App.Description, + Subcommands: ctx.App.Commands, + Flags: ctx.App.Flags, + } +} + +// createCliApp is re-implementation of urfave/cli method (in command.go): +// +// func (c Command) startApp(ctx *Context) error +// +// It lets us show the subcommands when help is executed like: +// +// step help foo +// step help foo bar +// ... +func createCliApp(ctx *cli.Context, cmd cli.Command) *cli.App { + app := cli.NewApp() + app.Metadata = ctx.App.Metadata + + // set the name and usage + app.Name = cmd.HelpName + app.HelpName = cmd.HelpName + + app.Usage = cmd.Usage + app.UsageText = cmd.UsageText + app.Description = cmd.Description + app.ArgsUsage = cmd.ArgsUsage + + // set CommandNotFound + app.CommandNotFound = ctx.App.CommandNotFound + app.CustomAppHelpTemplate = cmd.CustomHelpTemplate + + // set the flags and commands + app.Commands = cmd.Subcommands + app.Flags = cmd.Flags + + app.Version = ctx.App.Version + app.Compiled = ctx.App.Compiled + app.Author = ctx.App.Author + app.Email = ctx.App.Email + app.Writer = ctx.App.Writer + app.ErrWriter = ctx.App.ErrWriter + + // Do not show help or version on subcommands + app.HideHelp = true + app.HideVersion = true + + // bash completion + app.EnableBashCompletion = ctx.App.EnableBashCompletion + if cmd.BashComplete != nil { + app.BashComplete = cmd.BashComplete + } + + // set the actions + app.Before = cmd.Before + app.After = cmd.After + + if cmd.Action != nil { + app.Action = cmd.Action + } else { + app.Action = helpCommand.Action + } + app.OnUsageError = cmd.OnUsageError + + app.Setup() + return app +} diff --git a/command/crypto/crypto.go b/command/crypto/crypto.go new file mode 100644 index 00000000..791223f2 --- /dev/null +++ b/command/crypto/crypto.go @@ -0,0 +1,166 @@ +package crypto + +import ( + "github.com/smallstep/cli/command" + "github.com/smallstep/cli/command/crypto/hash" + "github.com/smallstep/cli/command/crypto/jwe" + "github.com/smallstep/cli/command/crypto/jwk" + "github.com/smallstep/cli/command/crypto/jwt" + "github.com/smallstep/cli/command/crypto/kdf" + "github.com/smallstep/cli/command/crypto/nacl" + "github.com/smallstep/cli/command/crypto/otp" + "github.com/urfave/cli" +) + +func init() { + cmd := cli.Command{ + Name: "crypto", + Usage: "useful cryptographic plumbing", + Description: `The **step crypto** command group provides a selection of useful cryptographic +primitives that balances completeness and safety (cryptographic strength, ease +of use, and misuse prevention). Subcommands include flags and arguments to +select algorithms and fine-tune behaviors, but we've selected safe defaults for +you wherever possible. + +Insecure or subtle cryptographic primitives and options are gated with flags to +prevent accidental misuse. Such primitives and options will not work unless you +pass the corresponding flags to indicate that you understand the risks +(**--insecure** and **--subtle**, respectively). Our rationale for these +decisions is usually documented in the **SECURITY CONSIDERATIONS** section of +the help for each subcommand. + +## SECURITY CONSIDERATIONS + +The strength of cryptographic mechanisms depends on the strength of all links +in the security chain. This includes the quality and strength of algorithms, +random number generation, distribution mechanisms, etc. It also includes +protection against hostile observation and tampering as well as the security of +the overall system including the operating system and personnel, etc. Where +possible, we've selected secure defaults. Whenever a subtle or insecure +cryptographic operation is attempted affirmative confirmation via prompt or +command line flag is required, indicating that you understand and accept the +risks. That said, many of these factors are beyond the scope of this tool. + +**Key Length** + +: This tool enforces a minimum key size of **256 bits for symmetric keys**, which is + generally considered quantum-safe and accepted as sufficient for the + foreseeable future. + +: This tool enforces the NIST recommended minimum key size of **2048 bits for RSA** + keys, which RSA claims is equivalent in strength to 112 bit symmetric keys and + is likely to be sufficient until 2030. An RSA key length of at least 3072 bits, + which RSA claims is equivalent to 128 bit symmetric keys, should be used if + security is required beyond 2030. + +: Elliptic curve cryptography is generally believed to be secure with shorter + keys than RSA requires. NIST guidelines state that ECC keys should be twice the + length of the equivalent strength symmetric key. The rough equivalencies for + the elliptic curves supported by this tool are: + +: | key type | curve | RSA equivalent | symmetric key equivalent | + |----------|---------|----------------|--------------------------| + | EC | P-256 | ~3000 bits | ~128 bits | + | EC | P-384 | ~4096 bits | ~192 bits | + | EC | P-521 | ~15000 bits | ~256 bits | + | OKP | Ed25519 | ~3000 bits | ~140 bits | + +: Elliptic curve cryptography has the additional advantages of much smaller key + sizes for equivalent security levels, and much faster cryptographic operations + compared to RSA. The strength of these keys is generally considered sufficient + for the predictable and foreseeable future. + +: Note that for cryptographic protocols that have perfect forward secrecry and + only use asymmetric keys for symmetric key negotiation your system will remain + secure against future threats as long as the keys are large enough that they + cannot be cracked today. In other words, sizing your keys to protect against + potential future threats is largely irrelevant. + +**Key Use** + +: In general you should not use an asymmetric keypair for both signing and + encryption. Using a single key for both operations can introduce attack vectors + that would not otherwise exist. Attacks aside, signing keys and encryption + keys generally have different life cycles. Signing keys are generally destroyed + once they're no longer useful for singing new data. Encryption keys, on the + other hand, must be retained as long as data exists that was encrypted for the + key. So using a signing key for encryption may force you to retain a signing + key for longer than it's needed, leaving it susceptible to misuse. + +: Raw public or private keys don't have any associated data, therefore this + tool cannot enforce key use on raw keys and this responsibility is up to + you. For keys in an "envelope" the envelope typically includes key use + restrictions (e.g., the "use" parameter in JWKs and the "Key Usage" + attribute of X.509 certificates). This tool generally requires key use to be + specified when creating an enveloped key, and enforces key use restrictions + when an enveloped key is being used. + +**Safe Curves** + +: There is some concern that certain standard elliptic curves are very hard to + implement correctly. These concerns are not purely theoretical. Implementation + issues have been uncovered and real attacks have been demonstrated. + +: While we take these concerns seriously, these curves are widely used in + practice, largely because they are perceived to be stronger than RSA and have + been implemented in more places than the "safe curves". Therefore, **we've + opted not to gate non-safe curves**. We've further elected to make **P-256** + the default curve for EC keys. + +: Still, it is important to be aware of the security risks assocated with their + risk. You should consider using "safe curves" if possible. We may change our + mind as support for safe curves improves. + +: Safe and non-safe curves implemented by this tool are: + +: | key type | curve | safe | + |----------|---------|------| + | EC | P-256 | NO | + | EC | P-384 | NO | + | EC | P-521 | NO | + | OKP | Ed25519 | YES | + +: For more information see https://safecurves.cr.yp.to/ + +**Quantum Safety** + +: Quantum-safe cryptography refers to keys and algorithms that are secure against + an attack by a quantum computer. As of 2018 most public key algorithms are not + quantum safe. In particular, **none of the public key algorithms implemented by + this tool are quantum safe**. However, no quantum computer exists that is + powerful enough to break current algorithms. Using cryptographic protocols with + forward secrecy is the best way to protect against future quantum attacks. + +**Forward Secrecy** + +: A cryptosystem or protocol has forward secrecy (or perfect forward secrecy) if, + for each session or interaction, a random key is generated such that an + attacker with access to all private keys would still not know the generated + key. This can be accomplished using Diffie-Hellman key exchange, for instance. + +: Forward secrecy can protect against an attacker who stores intercepted + communication and waits for your private key to be compromised, at which point + they could decrypt the stored communication. It also offers good protection + against quantum attacks since symmetric key cryptosystems like AES are already + considered quantum resistant with sufficiently large key sizes. The current + best quantum attack against symmetric key systems requires work proportional to + the square of the size of the key space. In other words, a symmetric key is + half as strong against a quantum attack vs. a conventional attack, so your key + needs to be twice as long for equivalent quantum-safe security. A 256 bit + symmetric key in the context of a quantum attack is equivalent in strength to a + 128 bit key in the context of a conventioanl attack. +`, + Subcommands: cli.Commands{ + jwk.Command(), + jwt.Command(), + jwe.Command(), + hash.Command(), + kdf.Command(), + nacl.Command(), + createKeyPairCommand(), + otp.Command(), + }, + } + + command.Register(cmd) +} diff --git a/command/crypto/hash/hash.go b/command/crypto/hash/hash.go new file mode 100644 index 00000000..ebc549a0 --- /dev/null +++ b/command/crypto/hash/hash.go @@ -0,0 +1,276 @@ +package hash + +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "crypto/subtle" + "encoding/binary" + "encoding/hex" + "fmt" + "hash" + "io" + "io/ioutil" + "os" + "path" + "strings" + + "github.com/pkg/errors" + "github.com/smallstep/cli/errs" + "github.com/urfave/cli" +) + +type hashConstructor func() hash.Hash + +// Command returns the jwk subcommand. +func Command() cli.Command { + return cli.Command{ + Name: "hash", + Usage: "generates and checks hashes of files and directories", + UsageText: "step crypto hash [SUBCOMMAND_FLAGS]", + Subcommands: cli.Commands{ + digestCommand(), + compareCommand(), + }, + } +} + +func digestCommand() cli.Command { + return cli.Command{ + Name: "digest", + Action: cli.ActionFunc(digestAction), + Usage: "generate a hash digest of a file or directory", + UsageText: "step crypto hash digest FILE_OR_DIRECTORY [--alg ALGORITHM]", + Description: `The 'step crypto hash digest' command generates a hash digest for a given +file or directory. For a file, the output is the same as tools like 'shasum'. +For directories, the tool computes a hash tree and outputs a single hash +digest. + +POSITIONAL ARGUMENTS + + FILE_OR_DIRECTORY + The path to a file or directory to hash.`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "alg", + Value: "sha256", + Usage: `The hash algorithm to use. + + ALGORITHM must be one of: + sha1 + sha224 + sha256 (default) + sha384 + sha512 + sha512-224 + sha512-256 + sha + md5 (requires '--insecure')`, + }, + cli.BoolFlag{ + Name: "insecure", + Hidden: true, + }, + }, + } +} + +func compareCommand() cli.Command { + return cli.Command{ + Name: "compare", + Action: cli.ActionFunc(compareAction), + Usage: "verify the hash digest for a file or directory matches an expected value", + UsageText: "step crypto hash compare HASH FILE_OR_DIRECTORY [--alg ALGORITHM]", + Description: `The 'step crypto hash compare' command verifies that the expected hash value +matches the computed hash value for a file or directory. + +POSITIONAL ARGUMENTS + + HASH + The expected hash digest + + FILE_OR_DIRECTORY + The path to a file or directory to hash.`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "alg", + Value: "sha256", + Usage: `The hash algorithm to use. + + ALGORITHM must be one of: + sha1 + sha224 + sha256 (default) + sha384 + sha512 + sha512-224 + sha512-256 + sha + md5 (requires '--insecure')`, + }, + cli.BoolFlag{ + Name: "insecure", + Hidden: true, + }, + }, + } +} + +func digestAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 1); err != nil { + return err + } + + hc, err := getHash(ctx, ctx.String("alg"), ctx.Bool("insecure")) + if err != nil { + return err + } + + filename := ctx.Args().Get(0) + st, err := os.Stat(filename) + if err != nil { + return errs.FileError(err, filename) + } + + var sum []byte + if st.IsDir() { + sum, err = hashDir(hc, filename) + } else { + sum, err = hashFile(hc(), filename) + } + if err != nil { + return err + } + + fmt.Printf("%x %s\n", sum, filename) + + return err +} + +func compareAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 2); err != nil { + return err + } + + hc, err := getHash(ctx, ctx.String("alg"), ctx.Bool("insecure")) + if err != nil { + return err + } + + hashStr := ctx.Args().Get(0) + hashBytes, err := hex.DecodeString(hashStr) + if err != nil { + return errs.Wrap(err, "error decoding %s", hashStr) + } + + filename := ctx.Args().Get(1) + st, err := os.Stat(filename) + if err != nil { + return errs.FileError(err, filename) + } + + var sum []byte + if st.IsDir() { + sum, err = hashDir(hc, filename) + } else { + sum, err = hashFile(hc(), filename) + } + if err != nil { + return err + } + + if subtle.ConstantTimeCompare(sum, hashBytes) == 1 { + fmt.Println("ok") + return nil + } + + return errors.New("fail") +} + +// getHash returns a new hash constructor for the given algorithm. MD5 +// algorithm can only be used if the insecure flag is passed. +func getHash(ctx *cli.Context, alg string, insecure bool) (hashConstructor, error) { + switch strings.ToLower(alg) { + case "sha", "sha1": + return func() hash.Hash { return sha1.New() }, nil + case "sha224": + return func() hash.Hash { return sha256.New224() }, nil + case "sha256": + return func() hash.Hash { return sha256.New() }, nil + case "sha384": + return func() hash.Hash { return sha512.New384() }, nil + case "sha512": + return func() hash.Hash { return sha512.New() }, nil + case "sha512-224": + return func() hash.Hash { return sha512.New512_224() }, nil + case "sha512-256": + return func() hash.Hash { return sha512.New512_256() }, nil + case "md5": + if insecure { + return func() hash.Hash { return md5.New() }, nil + } + return nil, errs.FlagValueInsecure(ctx, "alg", alg) + default: + return nil, errs.InvalidFlagValue(ctx, "alg", alg, "") + } +} + +// hashFile returns the hash of the given file using the given hash function. +func hashFile(h hash.Hash, filename string) ([]byte, error) { + f, err := os.Open(filename) + if err != nil { + return nil, errs.FileError(err, filename) + } + + if _, err := io.Copy(h, f); err != nil { + return nil, errs.FileError(err, filename) + } + + return h.Sum(nil), nil +} + +// hashDir creates a hash of a directory adding the following data to the +// hash: +// 1. Add directory mode bits to the hash +// 2. For each file/directory in directory: +// 2.1 If file: add file mode bits and sum +// 2.2 If directory: do hashDir and add sum +// 3. return sum +func hashDir(hc hashConstructor, dirname string) ([]byte, error) { + // ReadDir returns the entries sorted by filename + files, err := ioutil.ReadDir(dirname) + if err != nil { + return nil, errs.FileError(err, dirname) + } + st, err := os.Stat(dirname) + if err != nil { + return nil, errs.FileError(err, dirname) + } + + var sum []byte + mode := make([]byte, 4) + + // calculate sum of contents and mode + h := hc() + binary.LittleEndian.PutUint32(mode, uint32(st.Mode())) + h.Write(mode) + for _, fi := range files { + if fi.IsDir() { + sum, err = hashDir(hc, path.Join(dirname, fi.Name())) + if err != nil { + return nil, err + } + } else { + binary.LittleEndian.PutUint32(mode, uint32(fi.Mode())) + h.Write(mode) + sum, err = hashFile(hc(), path.Join(dirname, fi.Name())) + if err != nil { + return nil, err + } + } + h.Write(sum) + } + + return h.Sum(nil), nil +} diff --git a/command/crypto/internal/crypto/crypto.go b/command/crypto/internal/crypto/crypto.go new file mode 100644 index 00000000..778cc6ea --- /dev/null +++ b/command/crypto/internal/crypto/crypto.go @@ -0,0 +1,210 @@ +package crypto + +import ( + "bytes" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "fmt" + "io" + "io/ioutil" + "os" + + "github.com/pkg/errors" + "github.com/smallstep/cli/command/crypto/internal/utils" + "golang.org/x/crypto/ed25519" +) + +// GetRandomSalt generates a new salt of the given size. +func GetRandomSalt(size int) ([]byte, error) { + salt := make([]byte, size) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + return nil, errors.Wrap(err, "error generating salt") + } + return salt, nil +} + +// ReadCertificate returns a *x509.Certificate from the given filename. It +// supports certificates formats PEM and DER. +func ReadCertificate(filename string) (*x509.Certificate, error) { + b, err := ioutil.ReadFile(filename) + if err != nil { + return nil, errors.Wrapf(err, "error reading %s", filename) + } + + // PEM format + if bytes.HasPrefix(b, []byte("-----BEGIN ")) { + crt, err := ReadPEM(filename) + if err != nil { + return nil, err + } + switch crt := crt.(type) { + case *x509.Certificate: + return crt, nil + default: + return nil, errors.Errorf("error decoding PEM: file '%s' does not contain a certificate", filename) + } + } + + // DER format (binary) + crt, err := x509.ParseCertificate(b) + return crt, errors.Wrapf(err, "error parsing %s", filename) +} + +// ParsePEM returns the key or certificate PEM-encoded in the given bytes. +func ParsePEM(b []byte, filename string) (interface{}, error) { + block, rest := pem.Decode(b) + switch { + case block == nil: + return nil, errors.Errorf("error decoding PEM: file '%s' is not a valid PEM encoded key", filename) + case len(rest) > 0: + return nil, errors.Errorf("error decoding PEM: file '%s' contains more than one key", filename) + } + + // PEM is encrypted: ask for password + if block.Headers["Proc-Type"] == "4,ENCRYPTED" { + pass, err := utils.ReadPassword(fmt.Sprintf("Please enter the password to decrypt %s: ", filename)) + if err != nil { + return nil, err + } + + block.Bytes, err = x509.DecryptPEMBlock(block, pass) + if err != nil { + return nil, errors.Wrapf(err, "error decrypting %s", filename) + } + } + + switch block.Type { + case "PUBLIC KEY": + pub, err := ParsePKIXPublicKey(block.Bytes) + return pub, errors.Wrapf(err, "error parsing %s", filename) + case "RSA PRIVATE KEY": + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + return priv, errors.Wrapf(err, "error parsing %s", filename) + case "EC PRIVATE KEY": + priv, err := x509.ParseECPrivateKey(block.Bytes) + return priv, errors.Wrapf(err, "error parsing %s", filename) + case "PRIVATE KEY", "OPENSSH PRIVATE KEY": + priv, err := ParsePKCS8PrivateKey(block.Bytes) + return priv, errors.Wrapf(err, "error parsing %s", filename) + case "CERTIFICATE": + crt, err := x509.ParseCertificate(b) + return crt, errors.Wrapf(err, "error parsing %s", filename) + default: + return nil, errors.Errorf("error decoding PEM: file '%s' contains an unexpected header '%s'", filename, block.Type) + } +} + +// ReadPEM returns the key or certificated encoded in the given PEM encoded +// file. If the file is encrypted it will ask for a password and it will try +// to decrypt it. +// +// Supported keys algorithms are RSA and EC. Supported standards for private +// keys are PKCS#1, PKCS#8, RFC5915 for EC, and base64-encoded DER for +// certificates and public keys. +func ReadPEM(filename string) (interface{}, error) { + b, err := ioutil.ReadFile(filename) + if err != nil { + return nil, errors.Wrapf(err, "error reading %s", filename) + } + + return ParsePEM(b, filename) +} + +// pkcs8 reflects an ASN.1, PKCS#8 PrivateKey. See +// ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-8/pkcs-8v1_2.asn +// and RFC 5208. +type pkcs8 struct { + Version int + Algo pkix.AlgorithmIdentifier + PrivateKey []byte + // optional attributes omitted. +} + +type publicKeyInfo struct { + Raw asn1.RawContent + Algo pkix.AlgorithmIdentifier + PublicKey asn1.BitString +} + +// Algorithm Identifiers for Ed25519, Ed448, X25519 and X448 for use in the +// Internet X.509 Public Key Infrastructure +// https://tools.ietf.org/html/draft-ietf-curdle-pkix-10 +var ( + // oidX25519 = asn1.ObjectIdentifier{1, 3, 101, 110} + oidEd25519 = asn1.ObjectIdentifier{1, 3, 101, 112} +) + +// ParsePKCS8PrivateKey parses an unencrypted, PKCS#8 private key. See RFC +// 5208. +// +// Supported key types include RSA, ECDSA, and Ed25519. Unknown key types +// result in an error. +// +// On success, key will be of type *rsa.PrivateKey, *ecdsa.PublicKey, or +// ed25519.PrivateKey. +func ParsePKCS8PrivateKey(der []byte) (key interface{}, err error) { + var privKey pkcs8 + if _, err := asn1.Unmarshal(der, &privKey); err != nil { + return nil, err + } + + switch { + case privKey.Algo.Algorithm.Equal(oidEd25519): + seed := make([]byte, ed25519.SeedSize) + copy(seed, privKey.PrivateKey[2:]) + key = ed25519.NewKeyFromSeed(seed) + kk := key.(ed25519.PrivateKey) + fmt.Fprintf(os.Stderr, "% x\n% x\n", kk, kk.Public()) + return key, nil + // Prove of concept for key agreement algorithm X25519. + // A real implementation would use their own types. + // + // case privKey.Algo.Algorithm.Equal(oidX25519): + // k := make([]byte, ed25519.PrivateKeySize) + // var pub, priv [32]byte + // copy(priv[:], privKey.PrivateKey[2:]) + // curve25519.ScalarBaseMult(&pub, &priv) + // copy(k, priv[:]) + // copy(k[32:], pub[:]) + // key = ed25519.PrivateKey(k) + // return key, nil + default: + return x509.ParsePKCS8PrivateKey(der) + } +} + +// ParsePKIXPublicKey parses a DER encoded public key. These values are +// typically found in PEM blocks with "BEGIN PUBLIC KEY". +// +// Supported key types include RSA, DSA, ECDSA, and Ed25519. Unknown key types +// result in an error. +// +// On success, pub will be of type *rsa.PublicKey, *dsa.PublicKey, +// *ecdsa.PublicKey, or ed25519.PublicKey. +func ParsePKIXPublicKey(derBytes []byte) (pub interface{}, err error) { + var pki publicKeyInfo + if rest, err := asn1.Unmarshal(derBytes, &pki); err != nil { + return nil, err + } else if len(rest) != 0 { + return nil, errors.New("x509: trailing data after ASN.1 of public-key") + } + + switch { + case pki.Algo.Algorithm.Equal(oidEd25519): + pub = ed25519.PublicKey(pki.PublicKey.Bytes) + return pub, nil + // Prove of concept for key agreement algorithm X25519. + // A real implementation would use their own types. + // + // case pki.Algo.Algorithm.Equal(oidX25519): + // pub = ed25519.PublicKey(pki.PublicKey.Bytes) + // fmt.Fprintf(os.Stderr, "% x\n", pub) + // return pub, nil + default: + return x509.ParsePKIXPublicKey(derBytes) + } +} diff --git a/command/crypto/internal/jose/generate.go b/command/crypto/internal/jose/generate.go new file mode 100644 index 00000000..6bc3894a --- /dev/null +++ b/command/crypto/internal/jose/generate.go @@ -0,0 +1,185 @@ +package jose + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + + "github.com/pkg/errors" + "github.com/smallstep/cli/command/crypto/internal/crypto" + "github.com/smallstep/cli/command/crypto/internal/utils" + "golang.org/x/crypto/ed25519" +) + +// GenerateJWK generates a JWK given the key type, curve, alg, use, kid and +// the size of the RSA or oct keys if necessary. +func GenerateJWK(kty, crv, alg, use, kid string, size int) (jwk *JSONWebKey, err error) { + switch kty { + case "EC": + return generateECKey(crv, alg, use, kid) + case "RSA": + return generateRSAKey(size, alg, use, kid) + case "OKP": + return generateOKPKey(crv, alg, use, kid) + case "oct": + return generateOctKey(size, alg, use, kid) + default: + return nil, errors.Errorf("missing or invalid value for flag '--kty'") + } +} + +// GenerateJWKFromPEM returns an incomplete JSONWebKey using the key from a +// PEM file. +func GenerateJWKFromPEM(filename string) (*JSONWebKey, error) { + key, err := crypto.ReadPEM(filename) + if err != nil { + return nil, err + } + + switch key := key.(type) { + case *rsa.PrivateKey, *rsa.PublicKey: + return &JSONWebKey{Key: key}, nil + case *ecdsa.PrivateKey: + return &JSONWebKey{ + Key: key, + Algorithm: getECAlgorithm(key.Curve), + }, nil + case *ecdsa.PublicKey: + return &JSONWebKey{ + Key: key, + Algorithm: getECAlgorithm(key.Curve), + }, nil + case ed25519.PrivateKey, ed25519.PublicKey: + return &JSONWebKey{ + Key: key, + Algorithm: EdDSA, + }, nil + default: + return nil, errors.Errorf("error parsing %s: unsupported key type '%T'", filename, key) + } +} + +func generateECKey(crv, alg, use, kid string) (*JSONWebKey, error) { + var c elliptic.Curve + var sigAlg string + switch crv { + case P256, "": // default + c, sigAlg = elliptic.P256(), ES256 + case P384: + c, sigAlg = elliptic.P384(), ES384 + case P521: + c, sigAlg = elliptic.P521(), ES512 + default: + return nil, errors.Errorf("missing or invalid value for flag '--crv'") + } + + key, err := ecdsa.GenerateKey(c, rand.Reader) + if err != nil { + return nil, errors.Wrap(err, "error generating ECDSA key") + } + + switch use { + case "enc": + if alg == "" { + alg = string(DefaultECKeyAlgorithm) + } + default: + if alg == "" { + alg = sigAlg + } + } + + return &JSONWebKey{ + Key: key, + Algorithm: alg, + Use: use, + KeyID: kid, + }, nil +} + +func generateRSAKey(bits int, alg, use, kid string) (*JSONWebKey, error) { + if bits == 0 { + bits = DefaultRSASize + } + + key, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, errors.Wrap(err, "error generating RSA key") + } + + switch use { + case "enc": + if alg == "" { + alg = string(DefaultRSAKeyAlgorithm) + } + default: + if alg == "" { + alg = DefaultRSASigAlgorithm + } + } + + return &JSONWebKey{ + Key: key, + Algorithm: alg, + Use: use, + KeyID: kid, + }, nil +} + +func generateOKPKey(crv, alg, use, kid string) (*JSONWebKey, error) { + switch crv { + case Ed25519, "": // default + _, key, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, errors.Wrap(err, "error generating Ed25519 key") + } + + switch use { + case "enc": + return nil, errors.New("invalid algorithm: Ed25519 cannot be used for encryption") + default: + if alg == "" { + alg = EdDSA + } + } + + return &JSONWebKey{ + Key: key, + Algorithm: alg, + Use: use, + KeyID: kid, + }, nil + default: + return nil, errors.Errorf("missing or invalid value for flag '--crv'") + } +} + +func generateOctKey(size int, alg, use, kid string) (*JSONWebKey, error) { + if size == 0 { + size = DefaultOctSize + } + + key, err := utils.RandAlphanumeric(size) + if err != nil { + return nil, err + } + + switch use { + case "enc": + if alg == "" { + alg = string(DefaultOctKeyAlgorithm) + } + default: + if alg == "" { + alg = string(DefaultOctSigsAlgorithm) + } + } + + return &JSONWebKey{ + Key: []byte(key), + Algorithm: alg, + Use: use, + KeyID: kid, + }, nil +} diff --git a/command/crypto/internal/jose/parse.go b/command/crypto/internal/jose/parse.go new file mode 100644 index 00000000..7f70c031 --- /dev/null +++ b/command/crypto/internal/jose/parse.go @@ -0,0 +1,260 @@ +package jose + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/pkg/errors" + "github.com/smallstep/cli/command/crypto/internal/crypto" + "github.com/smallstep/cli/command/crypto/internal/utils" + "golang.org/x/crypto/ed25519" + jose "gopkg.in/square/go-jose.v2" +) + +type keyType int + +const ( + jwkKeyType keyType = iota + pemKeyType + octKeyType +) + +// MaxDecryptTries is the maximum number of attempts to decrypt a file. +const MaxDecryptTries = 3 + +// Decrypt returns the decrypted version of the given data if it's encrypted, +// it will return the raw data if it's not encrypted or the format is not +// valid. +func Decrypt(prompt string, data []byte) ([]byte, error) { + enc, err := jose.ParseEncrypted(string(data)) + if err != nil { + return data, nil + } + + // Decrypt flow + var pass []byte + for i := 0; i < MaxDecryptTries; i++ { + pass, err = utils.ReadPassword(prompt) + if err != nil { + return nil, err + } + + if data, err = enc.Decrypt(pass); err == nil { + return data, nil + } + } + + return nil, errors.Wrap(err, "failed to decrypt") +} + +// ParseKey returns a JSONWebKey from the given JWK file or a PEM file. For +// password protected keys, it will ask the user for a password. +func ParseKey(filename, use, alg, kid string, subtle bool) (*JSONWebKey, error) { + b, err := ioutil.ReadFile(filename) + if err != nil { + return nil, errors.Wrapf(err, "error reading %s", filename) + } + + jwk := new(JSONWebKey) + switch guessKeyType(alg, b) { + case jwkKeyType: + // Attempt to parse an encrypted file + prompt := fmt.Sprintf("Please enter the password to decrypt %s: ", filename) + if b, err = Decrypt(prompt, b); err != nil { + return nil, err + } + + // Unmarshal the plain (or decrypted JWK) + if err := json.Unmarshal(b, jwk); err != nil { + return nil, errors.Errorf("error reading %s: unsupported format", filename) + } + case pemKeyType: + if jwk.Key, err = crypto.ParsePEM(b, filename); err != nil { + return nil, err + } + case octKeyType: + jwk.Key = b + } + + // Validate key id + if kid != "" && jwk.KeyID != "" && kid != jwk.KeyID { + return nil, errors.Errorf("kid %s does not match the kid on %s", kid, filename) + } + if jwk.KeyID == "" { + jwk.KeyID = kid + } + if jwk.Use == "" { + jwk.Use = use + } + + // Set the algorithm if empty + guessJWKAlgorithm(jwk, alg) + + // Validate alg: if the flag '--subtle' is passed we will allow to overwrite it + if !subtle && alg != "" && jwk.Algorithm != "" && alg != jwk.Algorithm { + return nil, errors.Errorf("alg %s does not match the alg on %s", alg, filename) + } + if subtle && alg != "" { + jwk.Algorithm = alg + } + + return jwk, nil +} + +// ReadJWKSet reads a JWK Set from a URL or filename. URLs must start with "https://". +func ReadJWKSet(filename string) ([]byte, error) { + if strings.HasPrefix(filename, "https://") { + resp, err := http.Get(filename) + if err != nil { + return nil, errors.Wrapf(err, "error retrieving %s", filename) + } + defer resp.Body.Close() + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrapf(err, "error retrieving %s", filename) + } + return b, nil + } + b, err := ioutil.ReadFile(filename) + if err != nil { + return nil, errors.Wrapf(err, "error reading %s", filename) + } + return b, nil +} + +// ParseKeySet returns the JWK with the given key after parsing a JWKSet from +// a given file. +func ParseKeySet(filename, alg, kid string, isSubtle bool) (*jose.JSONWebKey, error) { + b, err := ReadJWKSet(filename) + if err != nil { + return nil, err + } + + // Attempt to parse an encrypted file + prompt := fmt.Sprintf("Please enter the password to decrypt %s: ", filename) + if b, err = Decrypt(prompt, b); err != nil { + return nil, err + } + + // Unmarshal the plain or decrypted JWKSet + jwkSet := new(jose.JSONWebKeySet) + if err := json.Unmarshal(b, jwkSet); err != nil { + return nil, errors.Errorf("error reading %s: unsupported format", filename) + } + + jwks := jwkSet.Key(kid) + switch len(jwks) { + case 0: + return nil, errors.Errorf("cannot find key with kid %s on %s", kid, filename) + case 1: + jwk := &jwks[0] + + // Set the algorithm if empty + guessJWKAlgorithm(jwk, alg) + + // Validate alg: if the flag '--subtle' is passed we will allow the + // overwrite of the alg + if !isSubtle && alg != "" && jwk.Algorithm != "" && alg != jwk.Algorithm { + return nil, errors.Errorf("alg %s does not match the alg on %s", alg, filename) + } + if isSubtle && alg != "" { + jwk.Algorithm = alg + } + return jwk, nil + default: + return nil, errors.Errorf("multiple keys with kid %s have been found on %s", kid, filename) + } +} + +// guessKeyType returns the key type of the given data. Key types are JWK, PEM +// or oct. +func guessKeyType(alg string, data []byte) keyType { + switch alg { + // jwk or file with oct data + case "HS256", "HS384", "HS512": + // Encrypted JWK ? + if _, err := jose.ParseEncrypted(string(data)); err == nil { + return jwkKeyType + } + // JSON JWK ? + if err := json.Unmarshal(data, &JSONWebKey{}); err == nil { + return jwkKeyType + } + // Default to oct + return octKeyType + default: + // PEM or default to JWK + if bytes.HasPrefix(data, []byte("-----BEGIN ")) { + return pemKeyType + } + return jwkKeyType + } +} + +// guessJWKAlgorithm set the algorithm if it's not set and we can guess it +func guessJWKAlgorithm(jwk *jose.JSONWebKey, defaultAlg string) { + if jwk.Algorithm == "" { + // Force default algorithm if passed. + if defaultAlg != "" { + jwk.Algorithm = defaultAlg + return + } + + // Use defaults for each key type + switch k := jwk.Key.(type) { + case []byte: + if jwk.Use == "enc" { + jwk.Algorithm = string(DefaultOctKeyAlgorithm) + } else { + jwk.Algorithm = string(DefaultOctSigsAlgorithm) + } + case *ecdsa.PrivateKey: + if jwk.Use == "enc" { + jwk.Algorithm = string(DefaultECKeyAlgorithm) + } else { + jwk.Algorithm = getECAlgorithm(k.Curve) + } + case *ecdsa.PublicKey: + if jwk.Use == "enc" { + jwk.Algorithm = string(DefaultECKeyAlgorithm) + } else { + jwk.Algorithm = getECAlgorithm(k.Curve) + } + case *rsa.PrivateKey, *rsa.PublicKey: + if jwk.Use == "enc" { + jwk.Algorithm = string(DefaultRSAKeyAlgorithm) + } else { + jwk.Algorithm = string(DefaultRSASigAlgorithm) + } + // Ed25519 can only be used for signing operations + case ed25519.PrivateKey, ed25519.PublicKey: + jwk.Algorithm = EdDSA + case *ed25519.PrivateKey, *ed25519.PublicKey: + jwk.Algorithm = EdDSA + } + } +} + +// getECAlgorithm returns the JWA algorithm name for the given elliptic curve. +// If the curve is not supported it will return an empty string. +// +// Supported curves are P-256, P-384, and P-521. +func getECAlgorithm(crv elliptic.Curve) string { + switch crv.Params().Name { + case P256: + return ES256 + case P384: + return ES384 + case P521: + return ES512 + default: + return "" + } +} diff --git a/command/crypto/internal/jose/types.go b/command/crypto/internal/jose/types.go new file mode 100644 index 00000000..3be60035 --- /dev/null +++ b/command/crypto/internal/jose/types.go @@ -0,0 +1,218 @@ +// Code generated (comment to force golint to ignore this file). DO NOT EDIT. + +package jose + +import ( + "time" + + jose "gopkg.in/square/go-jose.v2" + "gopkg.in/square/go-jose.v2/jwt" +) + +// SupportsPBKDF2 constant to know if the underlaying library supports +// password based cryptography algorithms. +const SupportsPBKDF2 = true + +// JSONWebToken represents a JSON Web Token (as specified in RFC7519). +type JSONWebToken = jwt.JSONWebToken + +// JSONWebKey represents a public or private key in JWK format. +type JSONWebKey = jose.JSONWebKey + +// JSONWebKeySet represents a JWK Set object. +type JSONWebKeySet = jose.JSONWebKeySet + +// JSONWebEncryption represents an encrypted JWE object after parsing. +type JSONWebEncryption = jose.JSONWebEncryption + +// Recipient represents an algorithm/key to encrypt messages to. +type Recipient = jose.Recipient + +// EncrypterOptions represents options that can be set on new encrypters. +type EncrypterOptions = jose.EncrypterOptions + +// Encrypter represents an encrypter which produces an encrypted JWE object. +type Encrypter = jose.Encrypter + +// ContentType represents type of the contained data. +type ContentType = jose.ContentType + +// KeyAlgorithm represents a key management algorithm. +type KeyAlgorithm = jose.KeyAlgorithm + +// ContentEncryption represents a content encryption algorithm. +type ContentEncryption = jose.ContentEncryption + +// SignatureAlgorithm represents a signature (or MAC) algorithm. +type SignatureAlgorithm = jose.SignatureAlgorithm + +// ErrCryptoFailure indicates an error in a cryptographic primitive. +var ErrCryptoFailure = jose.ErrCryptoFailure + +// Claims represents public claim values (as specified in RFC 7519). +type Claims = jwt.Claims + +// Builder is a utility for making JSON Web Tokens. Calls can be chained, and +// errors are accumulated until the final call to CompactSerialize/FullSerialize. +type Builder = jwt.Builder + +// NumericDate represents date and time as the number of seconds since the +// epoch, including leap seconds. Non-integer values can be represented +// in the serialized format, but we round to the nearest second. +type NumericDate = jwt.NumericDate + +// Audience represents the recipients that the token is intended for. +type Audience = jwt.Audience + +// Expected defines values used for protected claims validation. +// If field has zero value then validation is skipped. +type Expected = jwt.Expected + +// Signer represents a signer which takes a payload and produces a signed JWS object. +type Signer = jose.Signer + +// SigningKey represents an algorithm/key used to sign a message. +type SigningKey = jose.SigningKey + +// SignerOptions represents options that can be set when creating signers. +type SignerOptions = jose.SignerOptions + +// ErrInvalidIssuer indicates invalid iss claim. +var ErrInvalidIssuer = jwt.ErrInvalidIssuer + +// ErrInvalidAudience indicated invalid aud claim. +var ErrInvalidAudience = jwt.ErrInvalidAudience + +// ErrNotValidYet indicates that token is used before time indicated in nbf claim. +var ErrNotValidYet = jwt.ErrNotValidYet + +// ErrExpired indicates that token is used after expiry time indicated in exp claim. +var ErrExpired = jwt.ErrExpired + +// ErrInvalidSubject indicates invalid sub claim. +var ErrInvalidSubject = jwt.ErrInvalidSubject + +// ErrInvalidID indicates invalid jti claim. +var ErrInvalidID = jwt.ErrInvalidID + +// Key management algorithms +const ( + RSA1_5 = KeyAlgorithm("RSA1_5") // RSA-PKCS1v1.5 + RSA_OAEP = KeyAlgorithm("RSA-OAEP") // RSA-OAEP-SHA1 + RSA_OAEP_256 = KeyAlgorithm("RSA-OAEP-256") // RSA-OAEP-SHA256 + A128KW = KeyAlgorithm("A128KW") // AES key wrap (128) + A192KW = KeyAlgorithm("A192KW") // AES key wrap (192) + A256KW = KeyAlgorithm("A256KW") // AES key wrap (256) + DIRECT = KeyAlgorithm("dir") // Direct encryption + ECDH_ES = KeyAlgorithm("ECDH-ES") // ECDH-ES + ECDH_ES_A128KW = KeyAlgorithm("ECDH-ES+A128KW") // ECDH-ES + AES key wrap (128) + ECDH_ES_A192KW = KeyAlgorithm("ECDH-ES+A192KW") // ECDH-ES + AES key wrap (192) + ECDH_ES_A256KW = KeyAlgorithm("ECDH-ES+A256KW") // ECDH-ES + AES key wrap (256) + A128GCMKW = KeyAlgorithm("A128GCMKW") // AES-GCM key wrap (128) + A192GCMKW = KeyAlgorithm("A192GCMKW") // AES-GCM key wrap (192) + A256GCMKW = KeyAlgorithm("A256GCMKW") // AES-GCM key wrap (256) + PBES2_HS256_A128KW = KeyAlgorithm("PBES2-HS256+A128KW") // PBES2 + HMAC-SHA256 + AES key wrap (128) + PBES2_HS384_A192KW = KeyAlgorithm("PBES2-HS384+A192KW") // PBES2 + HMAC-SHA384 + AES key wrap (192) + PBES2_HS512_A256KW = KeyAlgorithm("PBES2-HS512+A256KW") // PBES2 + HMAC-SHA512 + AES key wrap (256) +) + +// Signature algorithms +const ( + HS256 = "HS256" // HMAC using SHA-256 + HS384 = "HS384" // HMAC using SHA-384 + HS512 = "HS512" // HMAC using SHA-512 + RS256 = "RS256" // RSASSA-PKCS-v1.5 using SHA-256 + RS384 = "RS384" // RSASSA-PKCS-v1.5 using SHA-384 + RS512 = "RS512" // RSASSA-PKCS-v1.5 using SHA-512 + ES256 = "ES256" // ECDSA using P-256 and SHA-256 + ES384 = "ES384" // ECDSA using P-384 and SHA-384 + ES512 = "ES512" // ECDSA using P-521 and SHA-512 + PS256 = "PS256" // RSASSA-PSS using SHA256 and MGF1-SHA256 + PS384 = "PS384" // RSASSA-PSS using SHA384 and MGF1-SHA384 + PS512 = "PS512" // RSASSA-PSS using SHA512 and MGF1-SHA512 + EdDSA = "EdDSA" // Ed25591 +) + +// Content encryption algorithms +const ( + A128CBC_HS256 = ContentEncryption("A128CBC-HS256") // AES-CBC + HMAC-SHA256 (128) + A192CBC_HS384 = ContentEncryption("A192CBC-HS384") // AES-CBC + HMAC-SHA384 (192) + A256CBC_HS512 = ContentEncryption("A256CBC-HS512") // AES-CBC + HMAC-SHA512 (256) + A128GCM = ContentEncryption("A128GCM") // AES-GCM (128) + A192GCM = ContentEncryption("A192GCM") // AES-GCM (192) + A256GCM = ContentEncryption("A256GCM") // AES-GCM (256) +) + +// Elliptic curves +const ( + P256 = "P-256" // P-256 curve (FIPS 186-3) + P384 = "P-384" // P-384 curve (FIPS 186-3) + P521 = "P-521" // P-521 curve (FIPS 186-3) +) + +// Ed25519 is the EdDSA signature scheme using SHA-512/256 and Curve25519 +const Ed25519 = "Ed25519" + +// Default key management, signature, and content encryption algorithms to use if none is specified. +const ( + // Key management algorithms + DefaultECKeyAlgorithm = ECDH_ES + DefaultRSAKeyAlgorithm = RSA_OAEP_256 + DefaultOctKeyAlgorithm = A256GCMKW + // Signature algorithms + DefaultRSASigAlgorithm = RS256 + DefaultOctSigsAlgorithm = HS256 + // Content encryption algorithm + DefaultEncAlgorithm = A256GCM +) + +// Default sizes +const ( + DefaultRSASize = 2048 + DefaultOctSize = 32 +) + +// ParseEncrypted parses an encrypted message in compact or full serialization format. +func ParseEncrypted(input string) (*JSONWebEncryption, error) { + return jose.ParseEncrypted(input) +} + +// NewEncrypter creates an appropriate encrypter based on the key type. +func NewEncrypter(enc ContentEncryption, rcpt Recipient, opts *EncrypterOptions) (Encrypter, error) { + return jose.NewEncrypter(enc, rcpt, opts) +} + +// NewNumericDate constructs NumericDate from time.Time value. +func NewNumericDate(t time.Time) NumericDate { + return jwt.NewNumericDate(t) +} + +// NewSigner creates an appropriate signer based on the key type +func NewSigner(sig SigningKey, opts *SignerOptions) (Signer, error) { + return jose.NewSigner(sig, opts) +} + +// ParseSigned parses token from JWS form. +func ParseSigned(s string) (*JSONWebToken, error) { + return jwt.ParseSigned(s) +} + +// Signed creates builder for signed tokens. +func Signed(sig Signer) Builder { + return jwt.Signed(sig) +} + +// Determine whether a JSONWebKey is symmetric +func IsSymmetric(k *JSONWebKey) bool { + switch k.Key.(type) { + case []byte: + return true + default: + return false + } +} + +// Determine whether a JSONWebKey is asymmetric +func IsAsymmetric(k *JSONWebKey) bool { + return !IsSymmetric(k) +} diff --git a/command/crypto/internal/jose/validate.go b/command/crypto/internal/jose/validate.go new file mode 100644 index 00000000..34262d90 --- /dev/null +++ b/command/crypto/internal/jose/validate.go @@ -0,0 +1,123 @@ +package jose + +import ( + "crypto/ecdsa" + "crypto/rsa" + "fmt" + + "github.com/pkg/errors" + "golang.org/x/crypto/ed25519" +) + +// ValidateJWK validates the given JWK. +func ValidateJWK(jwk *JSONWebKey) error { + switch jwk.Use { + case "sig": + return validateSigJWK(jwk) + case "enc": + return validateEncJWK(jwk) + default: + return validateGeneric(jwk) + } +} + +// validateSigJWK validates the given JWK for signature operations. +func validateSigJWK(jwk *JSONWebKey) error { + if jwk.Algorithm == "" { + return errors.New("flag '--alg' is required with the given key") + } + errctx := "the given key" + + switch k := jwk.Key.(type) { + case []byte: + switch jwk.Algorithm { + case HS256, HS384, HS512: + return nil + } + errctx = "kty 'oct'" + case *rsa.PrivateKey, *rsa.PublicKey: + switch jwk.Algorithm { + case RS256, RS384, RS512: + return nil + case PS256, PS384, PS512: + return nil + } + errctx = "kty 'RSA'" + case *ecdsa.PrivateKey: + curve := k.Params().Name + switch { + case jwk.Algorithm == ES256 && curve == P256: + return nil + case jwk.Algorithm == ES384 && curve == P384: + return nil + case jwk.Algorithm == ES512 && curve == P521: + return nil + } + errctx = fmt.Sprintf("kty 'EC' and crv '%s'", curve) + case *ecdsa.PublicKey: + curve := k.Params().Name + switch { + case jwk.Algorithm == ES256 && curve == P256: + return nil + case jwk.Algorithm == ES384 && curve == P384: + return nil + case jwk.Algorithm == ES512 && curve == P521: + return nil + } + errctx = fmt.Sprintf("kty 'EC' and crv '%s'", curve) + case ed25519.PrivateKey, ed25519.PublicKey: + if jwk.Algorithm == EdDSA { + return nil + } + errctx = "kty 'OKP' and crv 'Ed25519'" + } + + return errors.Errorf("alg '%s' is not compatible with %s", jwk.Algorithm, errctx) +} + +// validatesEncJWK validates the given JWK for encryption operations. +func validateEncJWK(jwk *JSONWebKey) error { + alg := KeyAlgorithm(jwk.Algorithm) + var kty string + + switch jwk.Key.(type) { + case []byte: + switch alg { + case DIRECT, A128GCMKW, A192GCMKW, A256GCMKW, A128KW, A192KW, A256KW: + return nil + } + kty = "oct" + case *rsa.PrivateKey, *rsa.PublicKey: + switch alg { + case RSA1_5, RSA_OAEP, RSA_OAEP_256: + return nil + } + kty = "RSA" + case *ecdsa.PrivateKey, *ecdsa.PublicKey: + switch alg { + case ECDH_ES, ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW: + return nil + } + kty = "EC" + case ed25519.PrivateKey, ed25519.PublicKey: + return errors.New("key Ed25519 cannot be used for encryption") + } + + return errors.Errorf("alg '%s' is not compatible with kty '%s'", jwk.Algorithm, kty) +} + +// validateGeneric validates just the supported key types. +func validateGeneric(jwk *JSONWebKey) error { + switch jwk.Key.(type) { + case []byte: + return nil + case *rsa.PrivateKey, *rsa.PublicKey: + return nil + case *ecdsa.PrivateKey, *ecdsa.PublicKey: + return nil + case ed25519.PrivateKey, ed25519.PublicKey: + return nil + } + + return errors.Errorf("unsupported key type '%T'", jwk.Key) +} diff --git a/command/crypto/internal/utils/keys.go b/command/crypto/internal/utils/keys.go new file mode 100644 index 00000000..d4e7ff09 --- /dev/null +++ b/command/crypto/internal/utils/keys.go @@ -0,0 +1,68 @@ +package utils + +// WritePublicKey encodes a crypto public key to a file on disk in PEM format. +import ( + "encoding/pem" + "os" + + "github.com/pkg/errors" + "github.com/smallstep/cli/crypto/keys" + "github.com/smallstep/cli/errs" +) + +// WritePublicKey encodes a crypto public key to a file on disk in PEM format. +// Any file with the same name will be overwritten. +func WritePublicKey(key interface{}, out string) error { + // Remove any file with same name, if it exists. + if _, err := os.Stat(out); err == nil { + if err = os.Remove(out); err != nil { + return errs.FileError(err, out) + } + } + keyOut, err := os.OpenFile(out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, + os.FileMode(0600)) + if err != nil { + return errs.FileError(err, out) + } + pubPEM, err := keys.PublicPEM(key) + if err != nil { + return errs.Wrap(err, + "failed to convert public key to PEM block") + } + err = pem.Encode(keyOut, pubPEM) + if err != nil { + return errs.Wrap(err, + "pem encode '%s' failed", out) + } + keyOut.Close() + return nil +} + +// WritePrivateKey encodes a crypto private key to a file on disk in PEM format. +// Any file with the same name will be overwritten. +func WritePrivateKey(key interface{}, pass, out string) error { + // Remove any file with same name, if it exists. + // Permissions on private key files may be such that overwriting them is impossible. + if _, err := os.Stat(out); err == nil { + if err = os.Remove(out); err != nil { + return errs.FileError(err, out) + } + } + keyOut, err := os.OpenFile(out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, + os.FileMode(0600)) + if err != nil { + return errs.FileError(err, out) + } + privPem, err := keys.PrivatePEM(key, keys.DefaultEncOpts(pass)) + if err != nil { + return errors.Wrap(err, + "failed to convert private key to PEM block") + } + err = pem.Encode(keyOut, privPem) + if err != nil { + return errors.Wrapf(err, + "pem encode '%s' failed", out) + } + keyOut.Close() + return nil +} diff --git a/command/crypto/internal/utils/random.go b/command/crypto/internal/utils/random.go new file mode 100644 index 00000000..266d6588 --- /dev/null +++ b/command/crypto/internal/utils/random.go @@ -0,0 +1,36 @@ +package utils + +import ( + "crypto/rand" + "math/big" + + "github.com/pkg/errors" +) + +// RandString returns a random string of a given length using the characters +// in the given string. It splits the string on runes to support UTF-8 +// characters. +func RandString(length int, chars string) (string, error) { + result := make([]rune, length) + runes := []rune(chars) + for i := range result { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(runes)))) + if err != nil { + return "", errors.Wrap(err, "error creating random number") + } + result[i] = runes[num.Int64()] + } + return string(result), nil +} + +// RandHex returns a random string of the given length using the hexadecimal +// characters in lower case (0-9+a-f). +func RandHex(length int) (string, error) { + return RandString(length, "0123456789abcdef") +} + +// RandAlphanumeric returns a random string of the given length using the 62 +// alphanumeric characters in the POSIX/C locale (a-z+A-Z+0-9). +func RandAlphanumeric(length int) (string, error) { + return RandString(length, "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") +} diff --git a/command/crypto/internal/utils/read.go b/command/crypto/internal/utils/read.go new file mode 100644 index 00000000..b643c86b --- /dev/null +++ b/command/crypto/internal/utils/read.go @@ -0,0 +1,71 @@ +package utils + +import ( + "bufio" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "syscall" + + "github.com/pkg/errors" + "golang.org/x/crypto/ssh/terminal" +) + +// ReadAll returns a slice of bytes with the content of the given reader. +func ReadAll(r io.Reader) ([]byte, error) { + b, err := ioutil.ReadAll(r) + return b, errors.Wrap(err, "error reading data") +} + +// ReadString reads one line from the given io.Reader. +func ReadString(r io.Reader) (string, error) { + br := bufio.NewReader(r) + str, err := br.ReadString('\n') + if err != nil && err != io.EOF { + return "", errors.Wrap(err, "error reading string") + } + return strings.TrimSpace(str), nil +} + +// ReadPassword asks the user for a password using the given prompt. If the +// program is receiving data from STDIN using a pipe, we cannot use +// terminal.ReadPassword on STDIN and we need to open the tty and read from +// it. +// +// This solution works on darwin and linux, but it might not work on other +// OSs. +func ReadPassword(prompt string) ([]byte, error) { + fmt.Fprint(os.Stderr, prompt) + var fd int + if terminal.IsTerminal(syscall.Stdin) { + fd = syscall.Stdin + } else { + tty, err := os.Open("/dev/tty") + if err != nil { + return nil, errors.Wrap(err, "error allocating terminal") + } + defer tty.Close() + fd = int(tty.Fd()) + } + + pass, err := terminal.ReadPassword(fd) + fmt.Fprintln(os.Stderr) + return pass, errors.Wrap(err, "error reading password") +} + +// ReadInput from stdin if something is detected or ask the user for an input +// using the given prompt. +func ReadInput(prompt string) ([]byte, error) { + st, err := os.Stdin.Stat() + if err != nil { + return nil, errors.Wrap(err, "error reading data") + } + + if st.Size() > 0 { + return ReadAll(os.Stdin) + } + + return ReadPassword(prompt) +} diff --git a/command/crypto/internal/utils/write.go b/command/crypto/internal/utils/write.go new file mode 100644 index 00000000..a34535f9 --- /dev/null +++ b/command/crypto/internal/utils/write.go @@ -0,0 +1,70 @@ +package utils + +import ( + "bufio" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "syscall" + + "github.com/pkg/errors" + "golang.org/x/crypto/ssh/terminal" +) + +var ( + // ErrFileExists is the error returned if a file exists. + ErrFileExists = errors.New("file exists") + + // ErrIsDir is the error returned if the file is a directory. + ErrIsDir = errors.New("file is a directory") +) + +// WriteFile wraps ioutil.WriteFile with a prompt to overwrite a file if the +// file exists. It returns ErrFileExists if the user picks to not overwrite +// the file. +func WriteFile(filename string, data []byte, perm os.FileMode) error { + st, err := os.Stat(filename) + if err != nil { + if os.IsNotExist(err) { + return ioutil.WriteFile(filename, data, perm) + } + return errors.Wrapf(err, "error reading information for %s", filename) + } + + if st.IsDir() { + return ErrIsDir + } + + // The file exists + var r io.Reader + if terminal.IsTerminal(syscall.Stdin) { + r = os.Stdin + } else { + tty, err := os.Open("/dev/tty") + if err != nil { + return errors.Wrap(err, "error allocating terminal") + } + r = tty + defer tty.Close() + } + + br := bufio.NewReader(r) + for cont := true; cont; { + fmt.Fprintf(os.Stderr, "Would you like to overwrite %s [Y/n]: ", filename) + str, err := br.ReadString('\n') + if err != nil { + return errors.Wrap(err, "error reading line") + } + str = strings.ToLower(strings.TrimSpace(str)) + switch str { + case "", "y", "yes": + cont = false + case "n", "no": + return ErrFileExists + } + } + + return ioutil.WriteFile(filename, data, perm) +} diff --git a/command/crypto/jwe/decrypt.go b/command/crypto/jwe/decrypt.go new file mode 100644 index 00000000..b6263751 --- /dev/null +++ b/command/crypto/jwe/decrypt.go @@ -0,0 +1,133 @@ +package jwe + +import ( + "fmt" + "os" + + "github.com/smallstep/cli/errs" + + "github.com/pkg/errors" + "github.com/smallstep/cli/command/crypto/internal/jose" + "github.com/smallstep/cli/command/crypto/internal/utils" + "github.com/urfave/cli" +) + +func decryptCommand() cli.Command { + return cli.Command{ + Name: "decrypt", + Action: cli.ActionFunc(decryptAction), + Usage: "verify a JWE and decrypt ciphertext", + UsageText: "step crypto jwe decrypt [--key JWK] [--jwks JWKS] [--kid KID]", + Description: `The 'step crypto jwe decrypt' command verifies a JWE read from STDIN and +decrypts the ciphertext printing it to STDOUT. If verification fails a +non-zero failure code is returned. If verification succeeds the command +returns 0.`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "key", + Usage: `The JWE recipient's private key. The KEY argument should be the name of a +file containing a private JWK (or a JWK encrypted as a JWE payload) or a PEM +encoded private key (or a private key encrypted using [TODO: insert private +key encryption mechanism]).`, + }, + cli.StringFlag{ + Name: "jwks", + Usage: `The JWK Set containing the recipient's private key. The JWKS argument should +be the name of a file. The file contents should be a JWK Set or a JWE with a +JWK Set payload. The '--jwks' flag requires the use of the '--kid' flag to +specify which key to use.`, + }, + cli.StringFlag{ + Name: "kid", + Usage: `The ID of the recipient's private key. KID is a case-sensitive string. When +used with '--jwk' the KID value must match the "kid" member of the JWK. When +used with '--jwks' (a JWK Set) the KID value must match the "kid" member of +one of the JWKs in the JWK Set.`, + }, + }, + } +} + +func decryptAction(ctx *cli.Context) error { + data, err := utils.ReadAll(os.Stdin) + if err != nil { + return err + } + + key := ctx.String("key") + jwks := ctx.String("jwks") + kid := ctx.String("kid") + + obj, err := jose.ParseEncrypted(string(data)) + if err != nil { + return errors.Wrap(err, "error parsing data") + } + + alg := jose.KeyAlgorithm(obj.Header.Algorithm) + + var isPBES2 bool + switch alg { + case jose.PBES2_HS256_A128KW, jose.PBES2_HS384_A192KW, jose.PBES2_HS512_A256KW: + isPBES2 = true + } + + switch { + case isPBES2 && key != "": + return errors.Errorf("flag '--key' cannot be used with JWE algorithm '%s'", alg) + case isPBES2 && jwks != "": + return errors.Errorf("flag '--jwks' cannot be used with JWE algorithm '%s'", alg) + case !isPBES2 && key == "" && jwks == "": + return errs.RequiredOrFlag(ctx, "key", "jwk") + case key != "" && jwks != "": + return errs.MutuallyExclusiveFlags(ctx, "key", "jwks") + case jwks != "" && kid == "": + return errs.RequiredWithFlag(ctx, "kid", "jwks") + } + + // Read key from --key or --jwks + var pbes2Key []byte + var jwk *jose.JSONWebKey + switch { + case key != "": + jwk, err = jose.ParseKey(key, "enc", "", kid, false) + case jwks != "": + jwk, err = jose.ParseKeySet(jwks, "", kid, false) + case isPBES2: + pbes2Key, err = utils.ReadPassword("Please enter the password to decrypt the content encryption key: ") + default: + return errs.RequiredOrFlag(ctx, "key", "jwk") + } + if err != nil { + return err + } + + var decryptKey interface{} + if isPBES2 { + decryptKey = pbes2Key + } else { + // Private keys are used for decryption + if jwk.IsPublic() { + return errors.New("cannot use a public key for decryption") + } + + if jwk.Use == "sig" { + return errors.New("invalid jwk use: found 'sig' (signature), expecting 'enc' (encryption)") + } + + // Validate jwk + if err := jose.ValidateJWK(jwk); err != nil { + return err + } + + decryptKey = jwk.Key + } + + decrypted, err := obj.Decrypt(decryptKey) + if err != nil { + return errors.Wrap(err, "error decrypting data") + } + + fmt.Printf(string(decrypted)) + + return nil +} diff --git a/command/crypto/jwe/encrypt.go b/command/crypto/jwe/encrypt.go new file mode 100644 index 00000000..7c46d636 --- /dev/null +++ b/command/crypto/jwe/encrypt.go @@ -0,0 +1,316 @@ +package jwe + +import ( + "fmt" + "os" + + "github.com/pkg/errors" + "github.com/smallstep/cli/command/crypto/internal/jose" + "github.com/smallstep/cli/command/crypto/internal/utils" + "github.com/smallstep/cli/errs" + "github.com/urfave/cli" +) + +func encryptCommand() cli.Command { + return cli.Command{ + Name: "encrypt", + Action: cli.ActionFunc(encryptAction), + Usage: "encrypt a payload using JSON Web Encryption (JWE)", + UsageText: `step crypto jwe encrypt [--alg KEY_ENC_ALGORITHM] [--enc CONTENT_ENC_ALGORITHM] + [--key JWK] [--jwks JWKS] [--kid KID]`, + Description: `The 'step crypto jwe encrypt' command encrypts a payload using JSON Web +Encryption (JWE). By default, the payload to encrypt is read from STDIN and +the JWE data structure will be written to STDOUT.`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "alg, algorithm", + Usage: `The cryptographic algorithm used to encrypt or determine the value of the +content encryption key (CEK). Algorithms are case-sensitive strings defined in +RFC7518. The selected algorithm must be compatible with the key type. This +flag is optional. If not specified, the 'alg' member of the JWK is used. If +the JWK has no "alg" member then a default is selected depending on the JWK +key type. If the JWK has an "alg" member and the "alg" flag is passed the two +options must match unless the '--subtle' flag is also passed. + + KEY_ENC_ALGORITHM is a case-sensitive string and must be one of: + + RSA1_5 + RSAES-PKCS1-v1_5 + RSA-OAEP + RSAES OAEP using default parameters + RSA-OAEP-256 + RSAES OAEP using SHA-256 and MGF1 with SHA-256 + A128KW + AES Key Wrap with default initial value using 128-bit key + A192KW + AES Key Wrap with default initial value using 192-bit key + A256KW + AES Key Wrap with default initial value using 256-bit key + dir + Direct use of a shared symmetric key as the content encryption key (CEK) + ECDH-ES (default for EC keys) + Elliptic Curve Diffie-Hellman Ephemeral Static key agreement + ECDH-ES+A128KW + ECDH-ES using Concat KDF and CEK wrapped with "A128KW" + ECDH-ES+A192KW + ECDH-ES using Concat KDF and CEK wrapped with "A192KW" + ECDH-ES+A256KW + ECDH-ES using Concat KDF and CEK wrapped with "A256KW" + A128GCMKW + Key wrappiung with AES GCM using 128-bit key + A192GCMKW + Key wrappiung with AES GCM using 192-bit key + A256GCMKW + Key wrappiung with AES GCM using 256-bit key + PBES2-HS256+A128KW + PBES2 with HMAC SHA-256 and "A128KW" wrapping + PBES2-HS384+A192KW + PBES2 with HMAC SHA-256 and "A192KW" wrapping + PBES2-HS512+A256KW + PBES2 with HMAC SHA-256 and "A256KW" wrapping`, + }, + cli.StringFlag{ + Name: "enc, encryption-algorithm", + Value: "A256GCM", + Usage: `The cryptographic content encryption algorithm used to perform authenticated +encryption on the plaintext payload (the content) to produce ciphertext and +the authentication tag. + + CONTENT_ENC_ALGORITHM is a case-sensitive string and must be one of: + + A128CBC-HS256 + AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm + A192CBC-HS384 + AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm + A256CBC-HS512 + AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm + A128GCM + AES GCM using 128-bit key + A192GCM + AES GCM using 192-bit key + A256GCM (default) + AES GCM using 256-bit key`, + }, + cli.StringFlag{ + Name: "key", + Usage: `The JWE recipient's public key. The KEY argument should be the name of a +file. JWEs can be encrypted for a recipient using a public JWK or a PEM +encoded public key.`, + }, + cli.StringFlag{ + Name: "jwks", + Usage: `The JWK Set containing the recipient's public key. The JWKS argument should +be the name of a file. The file contents should be a JWK Set. The '--jwks' +flag requires the use of the '--kid' flag to specify which key to use.`, + }, + cli.StringFlag{ + Name: "kid", + Usage: `The ID of the recipient's public key. KID is a case-sensitive string. When +used with '--key' the KID value must match the "kid" member of the JWK. When +used with '--jwks' (a JWK Set) the KID value must match the "kid" member of +one of the JWKs in the JWK Set.`, + }, + cli.StringFlag{ + Name: "typ, type", + Usage: `The media type of the JWE, used for disambiguation in applications where +more than one type of JWE may be processed. While this parameter might be +useful to applications, it is ignored by JWE implementations.`, + }, + cli.StringFlag{ + Name: "cty, content-type", + Usage: `The media type of the JWE payload, used for disambiguation of JWE objects in +applications where more than one JWE payload type may be present. This +parameter is ignored by JWE implementations, but may be processed by +applications that use JWE.`, + }, + cli.BoolFlag{ + Name: "subtle", + Hidden: true, + }, + }, + } +} + +func encryptAction(ctx *cli.Context) error { + data, err := utils.ReadAll(os.Stdin) + if err != nil { + return err + } + + // Validate parameters + alg, err := getRecipientAlg(ctx, ctx.String("alg")) + if err != nil { + return err + } + + enc, err := getContentEncryptionAlg(ctx, ctx.String("enc")) + if err != nil { + return err + } + + var isPBES2 bool + switch alg { + case jose.PBES2_HS256_A128KW, jose.PBES2_HS384_A192KW, jose.PBES2_HS512_A256KW: + isPBES2 = true + } + + key := ctx.String("key") + jwks := ctx.String("jwks") + kid := ctx.String("kid") + typ := ctx.String("typ") + cty := ctx.String("cty") + isSubtle := ctx.Bool("subtle") + + switch { + case isPBES2 && key != "": + return errs.MutuallyExclusiveFlags(ctx, "alg "+ctx.String("alg"), "key") + case isPBES2 && jwks != "": + return errs.MutuallyExclusiveFlags(ctx, "alg "+ctx.String("alg"), "jwks") + case !isPBES2 && key == "" && jwks == "": + return errs.RequiredOrFlag(ctx, "key", "jwk") + case key != "" && jwks != "": + return errs.MutuallyExclusiveFlags(ctx, "key", "jwks") + case jwks != "" && kid == "": + return errs.RequiredWithFlag(ctx, "kid", "jwks") + } + + // Read key from --key, --jwks, or a user provided + var pbes2Key []byte + var jwk *jose.JSONWebKey + switch { + case key != "": + jwk, err = jose.ParseKey(key, "enc", string(alg), kid, isSubtle) + case jwks != "": + jwk, err = jose.ParseKeySet(jwks, string(alg), kid, isSubtle) + case isPBES2: + pbes2Key, err = utils.ReadPassword("Please enter the password to encrypt the content encryption key: ") + default: + return errs.RequiredOrFlag(ctx, "key", "jwks") + } + if err != nil { + return err + } + + var recipient jose.Recipient + if isPBES2 { + recipient = jose.Recipient{ + Algorithm: alg, + Key: pbes2Key, + KeyID: kid, + } + } else { + // Public keys are used for encryption + jwkPub := jwk.Public() + jwk = &jwkPub + + if jwk.Use == "sig" { + return errors.New("invalid jwk use: found 'sig' (signature), expecting 'enc' (encryption)") + } + + // Validate jwk + if err := jose.ValidateJWK(jwk); err != nil { + return err + } + + // Prepare recipient + if alg == "" { + alg, err = getRecipientAlg(ctx, jwk.Algorithm) + if err != nil { + return err + } + } + + recipient = jose.Recipient{ + Algorithm: alg, + Key: jwk, + KeyID: kid, + } + } + + // Add extra headers + opts := new(jose.EncrypterOptions) + if typ != "" { + opts.WithType(jose.ContentType(typ)) + } + if cty != "" { + opts.WithContentType(jose.ContentType(cty)) + } + + // Encrypt + encrypter, err := jose.NewEncrypter(enc, recipient, opts) + if err != nil { + return errs.Wrap(err, "error creating cipher") + } + + obj, err := encrypter.Encrypt(data) + if err != nil { + return errs.Wrap(err, "error encrypting data") + } + + fmt.Println(obj.FullSerialize()) + return nil +} + +func getContentEncryptionAlg(ctx *cli.Context, enc string) (jose.ContentEncryption, error) { + switch enc { + case "": + return jose.A256GCM, nil + case "A128GCM": + return jose.A128GCM, nil + case "A192GCM": + return jose.A192GCM, nil + case "A256GCM": + return jose.A256GCM, nil + case "A128CBC-HS256": + return jose.A128CBC_HS256, nil + case "A192CBC-HS384": + return jose.A192CBC_HS384, nil + case "A256CBC-HS512": + return jose.A256CBC_HS512, nil + default: + return "", errs.InvalidFlagValue(ctx, "enc", enc, "") + } +} + +func getRecipientAlg(ctx *cli.Context, alg string) (jose.KeyAlgorithm, error) { + switch alg { + case "": + return "", nil + case "RSA1_5": + return jose.RSA1_5, nil + case "RSA-OAEP": + return jose.RSA_OAEP, nil + case "RSA-OAEP-256": + return jose.RSA_OAEP_256, nil + case "A128KW": + return jose.A128KW, nil + case "A192KW": + return jose.A192KW, nil + case "A256KW": + return jose.A256KW, nil + case "DIRECT": + return jose.DIRECT, nil + case "ECDH-ES": + return jose.ECDH_ES, nil + case "ECDH-ES+A128KW": + return jose.ECDH_ES_A128KW, nil + case "ECDH-ES+A192KW": + return jose.ECDH_ES_A192KW, nil + case "ECDH-ES+A256KW": + return jose.ECDH_ES_A256KW, nil + case "A128GCMKW": + return jose.A128GCMKW, nil + case "A192GCMKW": + return jose.A192GCMKW, nil + case "A256GCMKW": + return jose.A256GCMKW, nil + case "PBES2-HS256+A128KW": + return jose.PBES2_HS256_A128KW, nil + case "PBES2-HS384+A192KW": + return jose.PBES2_HS384_A192KW, nil + case "PBES2-HS512+A256KW": + return jose.PBES2_HS512_A256KW, nil + default: + return "", errs.InvalidFlagValue(ctx, "alg", alg, "") + } +} diff --git a/command/crypto/jwe/jwe.go b/command/crypto/jwe/jwe.go new file mode 100644 index 00000000..3aa85c8c --- /dev/null +++ b/command/crypto/jwe/jwe.go @@ -0,0 +1,96 @@ +package jwe + +import "github.com/urfave/cli" + +// Command returns the jwe subcommand. +func Command() cli.Command { + return cli.Command{ + Name: "jwe", + Usage: "encrypt and decrypt data and keys using JSON Web Encryption (JWE)", + UsageText: "step crypto jwe [arguments] [global-flags] [subcommand-flags]", + Description: `The 'step crypto jwe' command group provides facilities for encrypting and +decrypting content and representing encrypted content using JSON-based data +structures as defined by the JSON Web Encryption (JWE) specification in +RFC7516, using algorithms defined in the JSON Web Algorithms (JWA) +specification in RFC7518. A JWE is a data structure representing an encrypted +and integrity-protected message. + + There are two JWE serializations: the compact serialization is a small, URL- +safe representation that base64 encodes the JWE components. The compact +serialization is a URL-safe string, suitable for space-constrained +environments such as HTTP headers and URI query parameters. The JSON +serialization represents JWEs as JSON objects and allows the same content to +be encrypted to multiple parties (using multiple keys). + + A typical JWE in compact serialization is a dot-separated string with five +parts: + * Header: metadata describing how the plaintext payload was processed to + produce ciphertext (e.g., which algorithms were used to encrypt the + content encryption key and the plaintext payload) + * Encrypted Key: the "content encryption key" that was used to encrypt the + plaintext payload, encrypted for the JWE recipient(s) (see: "what's with + the encrypted key" below) + * Initialization Vector: an initialization vector for use with the specified + encryption algorithm, if applicable + * Ciphertext: the ciphertext value resulting produced from authenticated + encryption of the plaintext with additional authenticated data + * Authentication Tag: value resulting fromthe authenticated encryption of + the plaintext with additional authenticated data + +## What's with encrypted key? + + This is somewhat confusing. Instead of directly encrypting the plaintext +payload, JWE typically generates a new "content encryption key" then encrypts +*that key* for the intended recipient(s). Todo: y tho? + + While versatile, JWE is easy to use incorrectly. Therefore, any use of this +subcommand requires the use of the '--subtle' flag as a misuse prevention +mechanism. You should only use this subcommand if you know what you're doing. +If possible, you're better off using the higher level 'crypto nacl' command +group. + +EXAMPLES + + This example demonstrates how to produce a JWE for a recipient using the +RSA-OAEP algorithm to encrypt the content encryption key (producing the +encrypted key), and the A256GCM (AES GCM with 256-bit key) algorithm to +produce the ciphertext and authentication tag. + + 1. Encode the JWE header with the desired "alg" and "enc" members then + encode it producing the *header* + BASE64URL(UTF8({"alg":"RSA-OAEP","enc":"A256GCM"})) + => eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ + 2. Generate a random content encryption key (CEK), encrypt it using + RSA-OAEP, producing the *encrypted key* + 3. Generate a random initialization vector + 4. Perform authenticated encryption over the plaintext using the content + encryption key and A256GCM algorithm with the base64-encoded JWE headers + provided as additional authenticated data producing the *ciphertext* and + *authentication tag* + 5. Assemble the final result (compact serialization) to produce the string: + + BASE64URL(UTF8(header)) || '.' + || BASE64URL(encrypted key) || '.' + || BASE64URL(initialization vector) || '.' + || BASE64URL(ciphertext) || '.' + || BASE64URL(authentication tag) + + Producing a result like (line breaks for display purposes only): + + eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ. + OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe + ipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDb + Sv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaV + mqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je8 + 1860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi + 6UklfCpIMfIjf7iGdXKHzg. + 48V1_ALb6US04U3b. + 5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6ji + SdiwkIr3ajwQzaBtQD_A. + XFBoMYUZodetZdvTiFvSkQ`, + Subcommands: cli.Commands{ + encryptCommand(), + decryptCommand(), + }, + } +} diff --git a/command/crypto/jwk/create.go b/command/crypto/jwk/create.go new file mode 100644 index 00000000..c8df99bb --- /dev/null +++ b/command/crypto/jwk/create.go @@ -0,0 +1,619 @@ +package jwk + +import ( + "bytes" + gocrypto "crypto" + "encoding/base64" + "encoding/json" + "fmt" + "os" + + "github.com/pkg/errors" + "github.com/smallstep/cli/command/crypto/internal/crypto" + "github.com/smallstep/cli/command/crypto/internal/jose" + "github.com/smallstep/cli/command/crypto/internal/utils" + "github.com/urfave/cli" +) + +const ( + // 128-bit salt + pbkdf2SaltSize = 16 + // 100k iterations. Nist recommends at least 10k, 1Passsword uses 100k. + pbkdf2Iterations = 100000 +) + +func createCommand() cli.Command { + return cli.Command{ + Name: "create", + Action: cli.ActionFunc(createAction), + Usage: "create a JWK (JSON Web Key)", + UsageText: `**step crypto jwk create** + [**--kty**=] [**--alg**=] [**--use**=] + [**--size**=] [**--crv**=] [**--kid**=] + [**--from-pem**=]`, + Description: `**step crypto jwk create** generates a new JWK (JSON Web Key) or constructs a +JWK from an existing key. The generated JWK conforms to RFC7517 and can be used +to sign and encrypt data using JWT, JWS, and JWE. + +Files containing private keys are encrypted by default. You'll be prompted for +a password. Keys are written with file mode **0600** (i.e., readable and +writable only by the current user). + +All flags are optional. Defaults are suitable for most use cases. + +## POSITIONAL ARGUMENTS + + +: Path to which the the public JWK should be written + + +: Path to which the (JWE encrypted) private JWK should be written + +## EXIT CODES + +This command returns 0 on success and \>0 if any error occurs. + +## SECURITY CONSIDERATIONS + +All security considerations from **step help crypto** are relevant here. + +**Preventing hostile disclosure of non-public key material** + +: It is critical that any private and symmetric key material be protected from + unauthorized disclosure or modification. This includes the private key for + asymmetric key types (RSA, EC, and OKP) and the shared secret for symmetric key + types (oct). One means of protection is encryption. Keys can also be stored in + hardware or software "security enclaves" such as HSMs and TPMs or operating + system keychain management tools. + +**Key provenance and bindings** + +: Key provenance should always be scrutinized. You should not trust a key that + was obtained in an untrustworthy manner (e.g., non-TLS HTTP). + +: Usually applications use keys to make authorization decisions based on + attributes "bound" to the key such as the key owner's name or role. In these + scenarios the strength of the system's security depends on the strength of + these "bindings". There are a variety of mechanisms for securely binding + attributes to keys, including: + + * Cryptographically binding attributes to the public key using x509 + certificates (e.g., as defined in PKIX / RFC2580) + * Cryptographically binding attributes to the public key using JWTs + * Storing the public key or (hashed) shared secret along with the bound + attributes in a secure database + +: Cryptographic mechanisms require establishing a "root of trust" that can sign + the bindings (the certificates or JWTs) asserting that the bound attributes are + correct. + +## STANDARDS + +[RFC7517] +: Jones, M., "JSON Web Key (JWK)", https://tools.ietf.org/html/rfc7517 + +[RFC7518] +: Jones, M., "JSON Web Algorithms (JWA)", https://tools.ietf.org/html/rfc7518 + +[RFC7638] +: M. Jones, N. Sakimura., "JSON Web Key (JWK) Thumbprint", + https://tools.ietf.org/html/rfc7638 + +[RFC8037] +: I. Liusvaara., "CFRG Elliptic Curve Diffie-Hellman (ECDH) and Signatures in + JSON Object Signing and Encryption (JOSE)", + https://tools.ietf.org/html/rfc8037 + +## EXAMPLES + +Create a new JWK using default options: + +''' +$ step crypto jwk create jwk.pub.json jwk.json +''' + +Create an RSA JWK: + +''' +$ step crypto jwk create rsa.pub.json rsa.json --kty RSA +''' + +Create a symmetric key (oct key type): + +''' +$ step crypto jwk create oct.pub.json oct.json --kty oct +''' + +Create a key for use with the Ed25519 cryptosystem: + +''' +$ step crypto jwk create ed.pub.json ed.json \ + --kty OKP --crv Ed25519 +''' + +Create a key from an existing PEM file: + +''' +$ step crypto jwk create jwk.pub.json jwk.json + --from-pem key.pem +''' + +Create an 4096 bit RSA encryption key: + +''' +$ step crypto jwk create rsa-enc.pub.json rsa-enc.json \ + --kty RSA --size 4096 --use enc +''' + +Create a 192 bit symmetric encryption key for use with AES Key Wrap: + +''' +$ step crypto jwk create kw.pub.json kw.json \ + --kty oct --size 192 --use enc --alg A192GCMKW +''' +`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "kty, type", + Value: "EC", + Usage: `The of key to create. Corresponds to the **"kty"** JWK parameter. +If unset, default is EC. + +: is a case-sensitive string and must be one of: + + **EC** + : Create an **elliptic curve** keypair + + **oct** + : Create a **symmetric key** (octet stream) + + **OKP** + : Create an octet key pair (for **"Ed25519"** curve) + + **RSA** + : Create an **RSA** keypair +`, + }, + cli.IntFlag{ + Name: "size", + Usage: `The (in bits) of the key for RSA and oct key types. RSA keys require a +minimum key size of 2048 bits. If unset, default is 2048 bits for RSA keys and 128 bits for oct keys.`, + }, + cli.StringFlag{ + Name: "crv, curve", + Usage: `The elliptic to use for EC and OKP key types. Corresponds +to the **"crv"** JWK parameter. Valid curves are defined in JWA [RFC7518]. If +unset, default is P-256 for EC keys and Ed25519 for OKP keys. + +: is a case-sensitive string and must be one of: + + **P-256** + : NIST P-256 Curve + + **P-384** + : NIST P-384 Curve + + **P-521** + : NIST P-521 Curve + + **Ed25519** + : Ed25519 Curve +`, + }, + cli.StringFlag{ + Name: "alg, algorithm", + Usage: `The intended for use with this key. Corresponds to the +**"alg"** JWK parameter. is case-sensitive. If unset, the default +depends on the key use, key type, and curve (for EC and OKP keys). Defaults are: + +: | key type | use | curve | default algorithm | + |----------|-----|---------|-------------------| + | EC | sig | P-256 | ES256 | + | EC | sig | P-384 | ES384 | + | EC | sig | P-521 | ES512 | + | oct | sig | N/A | HS256 | + | RSA | sig | N/A | RS256 | + | OKP | sig | Ed25519 | EdDSA | + | EC | enc | P-256 | ECDH-ES | + | EC | enc | P-384 | ECDH-ES | + | EC | enc | P-521 | ECDH-ES | + | oct | enc | N/A | A256GCMKW | + | RSA | enc | N/A | RSA-OAP-256 | + +: If the key **"use"** is **"sig"** (signing) must be one of: + + **HS256** + : HMAC using SHA-256 + + **HS384** + : HMAC using SHA-384 + + **HS512** + : HMAC using SHA-512 + + **RS256** + : RSASSA-PKCS1-v1_5 using SHA-256 + + **RS384** + : RSASSA-PKCS1-v1_5 using SHA-384 + + **RS512** + : RSASSA-PKCS1-v1_5 using SHA-512 + + **ES256** + : ECDSA using P-256 and SHA-256 + + **ES384** + : ECDSA using P-384 and SHA-384 + + **ES512** + : ECDSA using P-521 and SHA-512 + + **PS256** + : RSASSA-PSS using SHA-256 and MGF1 with SHA-256 + + **PS384** + : RSASSA-PSS using SHA-384 and MGF1 with SHA-384 + + **PS512** + : RSASSA-PSS using SHA-512 and MGF1 with SHA-512 + + **EdDSA** + : EdDSA signature algorithm + +: If the key **"use"** is **"enc"** (encryption) must be one of: + + **RSA1_5** + : RSAES-PKCS1-v1_5 + + **RSA-OAEP** + : RSAES OAEP using default parameters + + **RSA-OAEP-256** + : RSAES OAEP using SHA-256 and MGF1 with SHA-256 + + **A128KW** + : AES Key Wrap with default initial value using 128-bit key + + **A192KW** + : AES Key Wrap with default initial value using 192-bit key + + **A256KW** + : AES Key Wrap with default initial value using 256-bit key + + **dir** + : Direct use of a shared symmetric key as the content encryption key (CEK) + + **ECDH-ES** + : Elliptic Curve Diffie-Hellman Ephemeral Static key agreement + + **ECDH-ES+A128KW** + : ECDH-ES using Concat KDF and CEK wrapped with "A128KW" + + **ECDH-ES+A192KW** + : ECDH-ES using Concat KDF and CEK wrapped with "A192KW" + + **ECDH-ES+A256KW** + : ECDH-ES using Concat KDF and CEK wrapped with "A256KW" + + **A128GCMKW** + : Key wrapping with AES GCM using 128-bit key + + **A192GCMKW** + : Key wrapping with AES GCM using 192-bit key + + **A256GCMKW** + : Key wrapping with AES GCM using 256-bit key + + **PBES2-HS256+A128KW** + : PBES2 with HMAC SHA-256 and "A128KW" wrapping + + **PBES2-HS384+A192KW** + : PBES2 with HMAC SHA-256 and "A192KW" wrapping + + **PBES2-HS512+A256KW** + : PBES2 with HMAC SHA-256 and "A256KW" wrapping`, + }, + cli.StringFlag{ + Name: "use", + Value: "sig", + Usage: `The intended of the public key. Corresponds to the "use" JWK parameter. +The "use" parameter indicates whether the public key is used for encrypting +data or verifying the signature on data. + +: is a case-sensitive string and may be one of: + + **sig** + : The public key is used for verifying signatures. + + **enc** + : The public key is used for encrypting data. + +: Other values may be used but the generated JWKs will not work for signing or +encryption with this tool.`, + }, + cli.StringFlag{ + Name: "kid", + Usage: `The (key ID) for this JWK. Corresponds to the +"kid" JWK parameter. Used to identify an individual key in a JWK Set, for +example. is a case-sensitive string. If unset, the JWK Thumbprint +[RFC7638] is used as . See **step help crypto jwk thumbprint** for more +information on JWK Thumbprints.`, + }, + cli.StringFlag{ + Name: "key-ops", + Hidden: true, // Not currently implemented + Usage: `The operation(s) for which the key is intended to be used. Corresponds to +the "key_ops" JWK parameter. The '--key-ops' flag can be used multiple times +to indicate multiple intended operations. + + can be one of the values defined in RFC7517: + sign + Compute digital signature or MAC + verify + Verify digital signature or MAC + encrypt + Encrypt content + decrypt + Decrypt content and validate decryption, if applicable + wrapKey + Encrypt key + unwrapKey + Decrypt key and validate decryption, if applicable + deriveKey + Derive key + deriveBits + Derive bits not to be used as a key + + The key operation values are case-sensitive strings. Other values may be +used, but values must not be duplicated. + + The '--use' and '--key-ops' flags cannot be used together without also +passing the '--subtle' flag. The '--subtle' flag allows both flags to be used +in a consistent way (e.g., '--key-ops=encrypt --key-ops=decrypt --use=enc'). +Multiple unrelated operations (e.g., '--key-ops=encrypt --key-ops=sign') or +inconsistent combinations of '--use' and '--key-ops' (e.g., '--use=enc +--key-ops=sign') are not allowed without also passing the '--insecure' flag +because of potential vulnerabilities associated with using the same key with +multiple algorithms. + + Related operations include: + sign + verify + encrypt + decrypt + wrapKey + unwrapKey + If multiple values are passed and at least one is a non-standard value the +'--subtle' flag is required as you must verify that the operations are +related.`, + }, + cli.StringSliceFlag{ + Name: "from-certificate", + Usage: `TODO: usage is missing.`, + Hidden: true, + }, + cli.StringFlag{ + Name: "from-pem", + Usage: `Create a JWK representing the key encoded in an +existing instead of creating a new key.`, + }, + cli.BoolFlag{ + Name: "no-password", + Usage: `Do not ask for a password to encrypt the JWK. Sensitive +key material will be written to disk unencrypted. This is not +recommended. Requires **--insecure** flag.`, + }, + cli.BoolFlag{ + Name: "subtle", + Hidden: true, + }, + cli.BoolFlag{ + Name: "insecure", + Hidden: true, + }, + }, + } +} + +func createAction(ctx *cli.Context) error { + switch ctx.NArg() { + case 0: + return errors.New("missing positional arguments 'PUB_FILE' 'PRIV_FILE'") + case 1: + return errors.New("missing positional argument 'PRIV_FILE'") + case 2: // ok + default: + return errors.New("too many positional arguments use only 'PUB_FILE' 'PRIV_FILE'") + } + + // Use password to protect private JWK by default + usePassword := true + if ctx.Bool("no-password") { + if ctx.Bool("insecure") { + usePassword = false + } else { + return errors.New("flag '--no-password' requires the '--insecure' flag") + } + } + + pubFile := ctx.Args().Get(0) + privFile := ctx.Args().Get(1) + if pubFile == privFile { + return errors.New("positional arguments 'PUB_FILE' 'PRIV_FILE' cannot be equal") + } + + kty := ctx.String("kty") + crv := ctx.String("crv") + alg := ctx.String("alg") + use := ctx.String("use") + kid := ctx.String("kid") + size := ctx.Int("size") + pemFile := ctx.String("from-pem") + + switch kty { + case "EC": + if ctx.IsSet("size") { + return errors.New("flag '--size' is incompatible with '--kty EC'") + } + case "RSA": + if ctx.IsSet("crv") { + return errors.New("flag '--crv' is incompatible with '--kty RSA'") + } + // If size is not set it will use a safe default + if ctx.IsSet("size") { + if size < 2048 && !ctx.Bool("insecure") { + return errors.New("minimum '--size' for RSA keys is 2048 bits without '--insecure' flag") + } + if size <= 0 { + return errors.New("flag '--size' must be >= 0") + } + } + case "OKP": + if ctx.IsSet("size") { + return errors.New("flag '--size' is incompatible with '--kty OKP'") + } + case "oct": + if ctx.IsSet("crv") { + return errors.New("flag '--crv' is incompatible with '--kty oct'") + } + // If size is not set it will use a safe default + if ctx.IsSet("size") { + if size < 16 && !ctx.Bool("insecure") { + return errors.New("minimum '--size' for oct keys is 16 bytes without '--insecure' flag") + } + if size <= 0 { + return errors.New("flag '--size' must be >= 0") + } + } + default: + return errors.New("missing or invalid value for flag '--kty'") + } + + // Generate or read secrets + var err error + var jwk *jose.JSONWebKey + switch { + case pemFile != "": + jwk, err = jose.GenerateJWKFromPEM(pemFile) + default: + jwk, err = jose.GenerateJWK(kty, crv, alg, use, kid, size) + } + + if err != nil { + return err + } + + if ctx.IsSet("kid") { + jwk.KeyID = ctx.String("kid") + } else { + // A hash of a symmetric key can leak information, so we only thumbprint asymmetric keys. + if kty != "oct" { + hash, err := jwk.Thumbprint(gocrypto.SHA256) + if err != nil { + return errors.Wrap(err, "error generating JWK thumbprint") + } + jwk.KeyID = base64.RawURLEncoding.EncodeToString(hash) + } + } + jwk.Use = use + + if jwk.Algorithm == "" { + jwk.Algorithm = alg + } + + if err := jose.ValidateJWK(jwk); err != nil { + return err + } + + // Add x5c (X.509 Certificate Chain) parameter + crtFiles := ctx.StringSlice("from-certificate") + for _, name := range crtFiles { + crt, err := crypto.ReadCertificate(name) + if err != nil { + return err + } + jwk.Certificates = append(jwk.Certificates, crt) + } + + var jwkPub jose.JSONWebKey + if jose.IsSymmetric(jwk) { + jwkPub = *jwk + } else { + jwkPub = jwk.Public() + } + + // Create and write public JWK + b, err := json.MarshalIndent(jwkPub, "", " ") + if err != nil { + return errors.Wrap(err, "error marshaling JWK") + } + if err := utils.WriteFile(pubFile, b, 0600); err != nil { + return errors.Wrap(err, "error creating JWK") + } + + if jwk.IsPublic() { + fmt.Fprintln(os.Stderr, "Only the public JWK was generated.") + fmt.Fprintln(os.Stderr, "Cannot retrieve a private key from a public one.") + return nil + } + + // Create and write private JWK + if usePassword { + var rcpt jose.Recipient + // Generate JWE encryption key. + if jose.SupportsPBKDF2 { + key, err := utils.ReadPassword("Please enter the password to encrypt the private JWK: ") + if err != nil { + return errors.Wrap(err, "error reading password") + } + + salt, err := crypto.GetRandomSalt(pbkdf2SaltSize) + if err != nil { + return err + } + + rcpt = jose.Recipient{ + Algorithm: jose.PBES2_HS256_A128KW, + Key: []byte(key), + P2C: pbkdf2Iterations, + P2S: salt, + } + } else { + key, err := utils.RandAlphanumeric(32) + if err != nil { + return errors.Wrap(err, "error generating password") + } + fmt.Printf("Private JWK file '%s' will be encrypted with the key:\n%s\n", privFile, key) + rcpt = jose.Recipient{Algorithm: jose.A128KW, Key: []byte(key)} + } + + b, err = json.Marshal(jwk) + if err != nil { + return errors.Wrap(err, "error marshaling JWK") + } + + encrypter, err := jose.NewEncrypter(jose.A128GCM, rcpt, nil) + if err != nil { + return errors.Wrap(err, "error creating cipher") + } + + obj, err := encrypter.Encrypt(b) + if err != nil { + return errors.Wrap(err, "error encrypting JWK") + } + + var out bytes.Buffer + if err := json.Indent(&out, []byte(obj.FullSerialize()), "", " "); err != nil { + return errors.Wrap(err, "error formatting JSON") + } + b = out.Bytes() + } else { + b, err = json.MarshalIndent(jwk, "", " ") + if err != nil { + return errors.Wrap(err, "error marshaling JWK") + } + } + if err := utils.WriteFile(privFile, b, 0600); err != nil { + return errors.Wrap(err, "error creating JWK") + } + + return nil +} diff --git a/command/crypto/jwk/jwk.go b/command/crypto/jwk/jwk.go new file mode 100644 index 00000000..59a98c10 --- /dev/null +++ b/command/crypto/jwk/jwk.go @@ -0,0 +1,34 @@ +package jwk + +import "github.com/urfave/cli" + +// Command returns the jwk subcommand. +func Command() cli.Command { + return cli.Command{ + Name: "jwk", + Usage: "create JWKs (JSON Web Keys) and manage JWK Key Sets", + UsageText: "step crypto jwk SUBCOMMAND [ARGUMENTS] [GLOBAL_FLAGS] [SUBCOMMAND_FLAGS]", + Description: `The **step crypto jwk** command group provides facilities for creating JWKs +(JSON Web Keys) as defined in RFC7517. It also includes command line utilities +for managing Key Sets and working with encrypted keys. + + A JWK is a JSON data structure that represents a cryptographic key. The +members of this data structure represent properties of the key, including its +value. A JWK Set is a simple data structure for representing a set of JWKs. A +JWK Set is a JSON object with a "keys" member whose value is an array of JWKs. +Cryptographic algorithms and identifiers for used by JWKs are defined by the +JSON Web Algorithms (JWA) specification in RFC7518. This tool also supports +extensions defined in standards track RFC8037 defining curve and algorithm +identifiers for Edwards-curve Digial Signatures. + + JWKs and JWK Sets are used in the JSON Web Signature (JWS; RFC7515) and JSON +Web Encryption (JWE; RFC7516) specifications for signing and encrypting JSON +data, respectively.`, + Subcommands: cli.Commands{ + createCommand(), + keysetCommand(), + publicCommand(), + thumbprintCommand(), + }, + } +} diff --git a/command/crypto/jwk/keyset.go b/command/crypto/jwk/keyset.go new file mode 100644 index 00000000..30210183 --- /dev/null +++ b/command/crypto/jwk/keyset.go @@ -0,0 +1,302 @@ +package jwk + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "syscall" + + "github.com/pkg/errors" + "github.com/smallstep/cli/command/crypto/internal/jose" + "github.com/urfave/cli" +) + +func keysetCommand() cli.Command { + return cli.Command{ + Name: "keyset", + Usage: "add, remove, and find JWKs in JWK Sets", + UsageText: "step crypto jwk set COMMAND [ARGUMENTS] [GLOBAL_FLAGS] [SUBCOMMAND_FLAGS]", + Description: `The 'step crypto jwk set' command group provides facilities for managing and +inspecting JWK Sets. A is a JSON object that represents a set of JWKs. They +are defined in RFC7517. + + A JWK Set is simply a JSON object with a "keys" member whose value is an +array of JWKs. Additional members are allowed in the object. They will be +preserved by this tool, but otherwise ignored. Duplicate member names are not +allowed.`, + Subcommands: cli.Commands{ + keysetAddCommand(), + keysetRemoveCommand(), + keysetListCommand(), + keysetFindCommand(), + }, + } +} + +func keysetAddCommand() cli.Command { + return cli.Command{ + Name: "add", + Action: cli.ActionFunc(keysetAddAction), + Usage: "a JWK to a JWK Set", + UsageText: "step crypto jwk set add JWKS_FILE", + Description: `The 'step crypto jwk set add' command reads a JWK from STDIN and +adds it to the JWK Set in JWKS_FILE. Modifications to JWKS_FILE are in-place. +The file is 'flock'd while it's being read and modified. + +POSITIONAL ARGUMENTS: + + JWKS_FILE + File containing a JWK Set`, + } +} + +func keysetRemoveCommand() cli.Command { + return cli.Command{ + Name: "remove", + Action: cli.ActionFunc(keysetRemoveAction), + Usage: "a JWK from a JWK Set", + UsageText: "step crypto jwk set remove JWKS_FILE --kid [KID]", + Description: `The 'step crypto jwk set remove' command removes the JWK with a key ID +matching KID from the JWK Set stored in JWKS_FILE. Modifications to JWKS_FILE +are in-place. The file is 'flock'd while it's being read and modified. + +POSITONAL_ARGUMENTS: + + JWKS_FILE + File containing a JWK Set`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "kid", + Usage: `The key ID of the JWK to remove from the JWK Set. KID is a case-sensitive +string.`, + }, + }, + } +} + +func keysetListCommand() cli.Command { + return cli.Command{ + Name: "list", + Action: cli.ActionFunc(keysetListAction), + Usage: "key IDs of JWKs in a JWK Set", + UsageText: "step crypto jwk set list JWKS_FILE", + Description: `The 'step crypto jwk set list' command lists the IDs ("kid" parameters) of +JWKs in a JWK Set. + +POSITONAL_ARGUMENTS: + + JWKS_FILE + File containing a JWK Set`, + } +} + +func keysetFindCommand() cli.Command { + return cli.Command{ + Name: "find", + Action: cli.ActionFunc(keysetFindAction), + Usage: "a JWK in a JWK Set", + UsageText: "step crypto jwk set find JWKS_FILE --kid [KID]", + Description: `The 'step crypto jwk set find' command locates the JWK with a key ID +matching KID from the JWK Set stored in JWKS_FILE. The matching JWK is printed +to STDOUT. + +POSITONAL_ARGUMENTS: + + JWKS_FILE + File containing a JWK Set`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "kid", + Usage: `The key ID of the JWK to remove from the JWK Set. KID is a case-sensitive +string.`, + }, + }, + } +} + +func keysetAddAction(ctx *cli.Context) error { + if ctx.NArg() != 1 { + return errors.Errorf("not enough positional arguments, use '%s'", ctx.Command.UsageText) + } + + b, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return errors.Wrap(err, "error reading STDIN") + } + + // Attempt to parse an encrypted file + if b, err = jose.Decrypt("Please enter the password to decrypt JWK: ", b); err != nil { + return err + } + + // Unmarshal the plain (or decrypted JWK) + var jwk jose.JSONWebKey + if err := json.Unmarshal(b, &jwk); err != nil { + return errors.New("error reading JWK: unsupported format") + } + + jwksFile := ctx.Args().Get(0) + jwks, writeFunc, err := rwLockKeySet(jwksFile) + if err != nil { + return err + } + + // According to RFC7517 there are cases where multiple keys can share the + // same "kid". One example is if they have different "kty" values but are + // considered to be equivalent alternatives by the application using them. + jwks.Keys = append(jwks.Keys, jwk) + return writeFunc(true) +} + +func keysetRemoveAction(ctx *cli.Context) error { + if ctx.NArg() != 1 { + return errors.Errorf("not enough positional arguments, use '%s'", ctx.Command.UsageText) + } + + kid := ctx.String("kid") + + jwksFile := ctx.Args().Get(0) + jwks, writeFunc, err := rwLockKeySet(jwksFile) + if err != nil { + return err + } + + // Filtering without allocating + keys := jwks.Keys[:0] + for _, key := range jwks.Keys { + if key.KeyID != kid { + keys = append(keys, key) + } + } + jwks.Keys = keys + return writeFunc(true) +} + +func keysetListAction(ctx *cli.Context) error { + if ctx.NArg() != 1 { + return errors.Errorf("not enough positional arguments, use '%s'", ctx.Command.UsageText) + } + + jwksFile := ctx.Args().Get(0) + jwks, writeFunc, err := rwLockKeySet(jwksFile) + if err != nil { + return err + } + + for _, key := range jwks.Keys { + fmt.Println(key.KeyID) + } + + return writeFunc(false) +} + +func keysetFindAction(ctx *cli.Context) error { + if ctx.NArg() != 1 { + return errors.Errorf("not enough positional arguments, use '%s'", ctx.Command.UsageText) + } + + kid := ctx.String("kid") + + jwksFile := ctx.Args().Get(0) + jwks, writeFunc, err := rwLockKeySet(jwksFile) + if err != nil { + return err + } + + for _, key := range jwks.Keys { + if key.KeyID == kid { + b, err := json.MarshalIndent(key, "", " ") + if err != nil { + return errors.Wrap(err, "error marshaling JWK") + } + fmt.Println(string(b)) + } + } + + return writeFunc(false) +} + +func rwLockKeySet(filename string) (jwks *jose.JSONWebKeySet, writeFunc func(bool) error, err error) { + var f *os.File + + f, err = os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + err = errors.Wrapf(err, "error reading %s", filename) + return + } + + fd := int(f.Fd()) + + // non-blocking exclusive lock + err = syscall.Flock(fd, syscall.LOCK_EX|syscall.LOCK_NB) + switch err { + case nil: // continue + case syscall.EWOULDBLOCK: + f.Close() + err = errors.Errorf("%s is locked", filename) + return + default: + f.Close() + err = errors.Wrapf(err, "error locking %s", filename) + return + } + + // close and unlock file on errors + defer func() { + if err != nil { + syscall.Flock(fd, syscall.LOCK_UN) + f.Close() + } + }() + + // Read key set + var b []byte + b, err = ioutil.ReadAll(f) + if err != nil { + err = errors.Wrapf(err, "error reading %s", filename) + return + } + + // Unmarshal the plain JWKSet + jwks = new(jose.JSONWebKeySet) + if len(b) > 0 { + if err = json.Unmarshal(b, jwks); err != nil { + err = errors.Wrapf(err, "error reading %s", filename) + return + } + } + + writeFunc = func(write bool) (err error) { + if write { + if b, err1 := json.MarshalIndent(jwks, "", " "); err1 != nil { + err = errors.Wrapf(err1, "error marshaling %s", filename) + } else { + if err1 := f.Truncate(0); err1 != nil { + err = errors.Wrapf(err1, "error writing %s", filename) + } else { + n, err1 := f.WriteAt(b, 0) + switch { + case err1 != nil: + err = errors.Wrapf(err1, "error writing %s", filename) + case n < len(b): + err = errors.Wrapf(io.ErrShortWrite, "error writing %s", filename) + } + } + } + } + + if err1 := syscall.Flock(fd, syscall.LOCK_UN); err1 != nil { + err = errors.Wrapf(err1, "error unlocking %s", filename) + } + + if err1 := f.Close(); err1 != nil { + err = errors.Wrapf(err1, "error closing %s", filename) + } + + return err + } + + return +} diff --git a/command/crypto/jwk/public.go b/command/crypto/jwk/public.go new file mode 100644 index 00000000..b947c1eb --- /dev/null +++ b/command/crypto/jwk/public.go @@ -0,0 +1,50 @@ +package jwk + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + + "github.com/smallstep/cli/command/crypto/internal/jose" + + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +func publicCommand() cli.Command { + return cli.Command{ + Name: "public", + Action: cli.ActionFunc(publicAction), + Usage: "extract a public JSON Web Key (JWK) from a private JWK", + UsageText: `step crypto jwk public`, + Description: `The 'step crypto jwk public' command reads a JWK from STDIN, derives +the corresponding public JWK, and prints the derived JWK to STDOUT.`, + } +} + +func publicAction(ctx *cli.Context) error { + b, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return errors.Wrap(err, "error reading from STDIN") + } + + jwk := new(jose.JSONWebKey) + // Attempt to decrypt if encrypted + if b, err = jose.Decrypt("Please enter the password to decrypt your private JWK: ", b); err != nil { + return err + } + + // Unmarshal the plain (or decrypted JWK) + if err := json.Unmarshal(b, jwk); err != nil { + return errors.New("error reading JWK: unsupported format") + } + + b, err = json.MarshalIndent(jwk.Public(), "", " ") + if err != nil { + return errors.Wrap(err, "error marshaling JWK") + } + + fmt.Println(string(b)) + return nil +} diff --git a/command/crypto/jwk/thumbprint.go b/command/crypto/jwk/thumbprint.go new file mode 100644 index 00000000..f478a2de --- /dev/null +++ b/command/crypto/jwk/thumbprint.go @@ -0,0 +1,51 @@ +package jwk + +import ( + "crypto" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "os" + + "github.com/pkg/errors" + "github.com/smallstep/cli/command/crypto/internal/jose" + "github.com/urfave/cli" +) + +func thumbprintCommand() cli.Command { + return cli.Command{ + Name: "thumbprint", + Action: cli.ActionFunc(thumbprintAction), + Usage: "compute thumbprint for a JWK", + UsageText: `step crypto jwk thumbprint`, + Description: `The 'step crypto jwk thumbprint' command reads a JWK from STDINT, derives the +corresponding JWK Thumbprint (RFC7638), and prints the base64-urlencoded +thumbprint to STDOUT.`, + } +} + +func thumbprintAction(ctx *cli.Context) error { + b, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return errors.Wrap(err, "error reading from STDIN") + } + + jwk := new(jose.JSONWebKey) + // Attempt to decrypt if encrypted + if b, err = jose.Decrypt("Please enter the password to decrypt your private JWK: ", b); err != nil { + return err + } + + // Unmarshal the plain (or decrypted JWK) + if err := json.Unmarshal(b, jwk); err != nil { + return errors.New("error reading JWK: unsupported format") + } + + hash, err := jwk.Thumbprint(crypto.SHA256) + if err != nil { + return errors.Wrap(err, "error generating JWK thumbprint") + } + fmt.Println(base64.RawURLEncoding.EncodeToString(hash)) + return nil +} diff --git a/command/crypto/jwt/inspect.go b/command/crypto/jwt/inspect.go new file mode 100644 index 00000000..496c00e3 --- /dev/null +++ b/command/crypto/jwt/inspect.go @@ -0,0 +1,76 @@ +package jwt + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/pkg/errors" + "github.com/smallstep/cli/command/crypto/internal/utils" + "github.com/smallstep/cli/errs" + "github.com/urfave/cli" +) + +func inspectCommand() cli.Command { + return cli.Command{ + Name: "inspect", + Action: cli.ActionFunc(inspectAction), + Usage: `return the decoded JWT without verification`, + UsageText: `step crypto jwt inspect --insecure`, + Description: `The 'step crypto jwt inspect' command reads a JWT data structure from STDIN, +decodes it, and outputs the header and payload on STDERR. Since this command +does not verify the JWT you must pass '--insecure' as a misuse prevention +mechanism.`, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "insecure", + Hidden: true, + }, + }, + } +} + +func inspectAction(ctx *cli.Context) error { + token, err := utils.ReadString(os.Stdin) + if err != nil { + return err + } + + if !ctx.Bool("insecure") { + return errs.InsecureCommand(ctx) + } + + return printToken(token) +} + +func printToken(token string) error { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return errors.New("error decoding token: JWT must have three parts") + } + + header, err := base64.RawURLEncoding.DecodeString(parts[0]) + if err != nil { + return errors.Wrapf(err, "error decoding token") + } + + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return errors.Wrapf(err, "error decoding token") + } + + m := make(map[string]json.RawMessage) + m["header"] = header + m["payload"] = payload + m["signature"] = []byte(`"` + parts[2] + `"`) + + b, err := json.MarshalIndent(m, "", " ") + if err != nil { + return errors.Wrapf(err, "error marshaling token data") + } + fmt.Fprintln(os.Stderr, string(b)) + + return nil +} diff --git a/command/crypto/jwt/jwt.go b/command/crypto/jwt/jwt.go new file mode 100644 index 00000000..c161930f --- /dev/null +++ b/command/crypto/jwt/jwt.go @@ -0,0 +1,37 @@ +package jwt + +import ( + "github.com/urfave/cli" +) + +// Command returns the cli.Command for jwt and related subcommands. +func Command() cli.Command { + return cli.Command{ + Name: "jwt", + Usage: "sign and verify data using JSON Web Tokens (JWT)", + UsageText: "step crypto jwt SUBCOMMAND [SUBCOMMAND_ARGUMENTS] [GLOBAL_FLAGS] [SUBCOMMAND_FLAGS]", + Description: `A JSON Web Token or JWT (pronounced "jot") is a compact data structure used +to represent some JSON encoded "claims" that are passed as the payload of a +JWS or JWE structure, enabling the claims to be digitally signed and/or +encrypted. The "claims" (or "claim set") are represented as an ordinary JSON +object. JWTs are represented using a compact format that's URL safe and can be +used in space-constrained environments. JWTs can be passed in HTTP +Authorization headers and as URI query parameters. + + A "claim" is a piece of information asserted about a subject, represented as +a key/value pair. Logically a verified JWT can be interpreted as "ISSUER says +to AUDIENCE that SUBJECT's CLAIM_NAME is CLAIM_VALUE" for each claim. + + A JWT signed using JWS has three parts: + 1. A base64 encoded JSON object representing the JOSE (JSON Object Signing + and Encryption) header that describes the cryptographic operations + applied to the JWT Claims Set + 2. A base64 encoded JSON object representing the JWT Claims Set + 3. A base64 encoded digital signature of message authentication code`, + Subcommands: cli.Commands{ + signCommand(), + verifyCommand(), + inspectCommand(), + }, + } +} diff --git a/command/crypto/jwt/sign.go b/command/crypto/jwt/sign.go new file mode 100644 index 00000000..3303f0e2 --- /dev/null +++ b/command/crypto/jwt/sign.go @@ -0,0 +1,340 @@ +package jwt + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/cli/command/crypto/internal/jose" + "github.com/smallstep/cli/command/crypto/internal/utils" + "github.com/smallstep/cli/errs" + "github.com/urfave/cli" +) + +func signCommand() cli.Command { + return cli.Command{ + Name: "sign", + Action: cli.ActionFunc(signAction), + Usage: "create a signed JWT data structure", + UsageText: `step crypto jwt sign [- | FILENAME] [--alg ALGORITHM] [--aud AUDIENCE] [--iss ISSUER] [--sub SUB] + [--exp EXPIRATION] [--iat ISSUED_AT] [--nbf NOT_BEFORE] [--key JWK] + [--jwks JWKS] [--kid KID] [--jti JTI]`, + Description: `The 'step crypto jwt sign' command generates a signed JSON Web Token (JWT) +by computing a digital signature or message authentication code for a JSON +payload. By default, the payload to sign is read from STDIN and the JWT will +be written to STDOUT. The suggested pronunciation of JWT is the same as the +English word "jot". + + A JWT is a compact data structure used to represent some JSON encoded +"claims" that are passed as the payload of a JWS or JWE structure, enabling +the claims to be digitally signed and/or encrypted. The "claims" (or "claim +set") are represented as an ordinary JSON object. JWTs are represented using a +compact format that's URL safe and can be used in space-constrained +environments. JWTs can be passed in HTTP Authorization headers and as URI +query parameters. + + A "claim" is a piece of information asserted about a subject, represented as +a key/value pair. Logically a verified JWT should be interpreted as "ISSUER +says to AUDIENCE that SUBJECT's CLAIM_NAME is CLAIM_VALUE" for each claim. + + Some optional arguments introduce subtle security considerations if omitted. +These considerations should be carefully analyzed. Therefore, omitting SUBTLE +arguments requires the use of the '--subtle' flag as a misuse prevention +mechanism. + + A JWT signed using JWS has three parts: + 1. A base64 encoded JSON object representing the JOSE (JSON Object Signing + and Encryption) header that describes the cryptographic operations + applied to the JWT Claims Set + 2. A base64 encoded JSON object representing the JWT Claims Set + 3. A base64 encoded digital signature of message authentication code`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "alg, algorithm", + Usage: `The signature or MAC algorithm to use. Algorithms are case-sensitive strings +defined in RFC7518. The selected algorithm must be compatible with the key +type. This flag is optional. If not specified, the "alg" member of the JWK is +used. If the JWK has no "alg" member then a default is selected depending on +the JWK key type. If the JWK has an "alg" member and the "alg" flag is passed +the two options must match unless the '--subtle' flag is also passed. + + ALGORITHM is a case-sensitive string and must be one of: + + HS256 + HMAC using SHA-256 (default for "oct" key type) + HS384 + HMAC using SHA-384 + HS512 + HMAC using SHA-512 + RS256 + RSASSA-PKCS1-v1_5 using SHA-256 (default for "RSA" key type) + RS384 + RSASSA-PKCS1-v1_5 using SHA-384 + RS512 + RSASSA-PKCS1-v1_5 using SHA-512 + ES256 + ECDSA using P-256 and SHA-256 (default for "EC" key type) + ES384 + ECDSA using P-384 and SHA-384 + ES512 + ECDSA using P-521 and SHA-512 + PS256 + RSASSA-PSS using SHA-256 and MGF1 with SHA-256 + PS384 + RSASSA-PSS using SHA-384 and MGF1 with SHA-384 + PS512 + RSASSA-PSS using SHA-512 and MGF1 with SHA-512 + EdDSA + EdDSA signature algorithm`, + }, + cli.StringFlag{ + Name: "iss, issuer", + Usage: `The issuer of this JWT. The processing of this claim is generally +application specific. Typically, the ISSUER must match the name of some +trusted entity (e.g., an identity provider like "https://accounts.google.com") +and identify which key(s) to use for JWT verification and/or decryption (e.g., +the keys at "https://www.googleapis.com/oauth2/v3/certs"). ISSUER is a +case-sensitive string.`, + }, + cli.StringSliceFlag{ + Name: "aud, audience", + Usage: `The intended recipient(s) of the JWT, encoded as the "aud" claim in the JWT. +Recipient(s) must identify themselves with one or more of the values in the +"aud" claim. The "aud" claim can be a string (indicating a single recipient) +or an array (indicating multiple potential recipients). This flag can be used +multiple times to generate a JWK with multiple intended recipients. Each +AUDIENCE is a case-sensitive string.`, + }, + cli.StringFlag{ + Name: "sub, subject", + Usage: `The subject of this JWT. The "claims" are normally interpreted as statements +about this subject. The subject must either be locally unique in the context +of the issuer or globally unique. The processing of this claim is generally +application specific. SUBJECT is a case-sensitive string.`, + }, + cli.Int64Flag{ + Name: "exp, expiration", + Usage: `The expiration time on or after which the JWT must not be accepted. EXPIRATION +must be a numeric value representing a Unix timestamp.`, + }, + cli.Int64Flag{ + Name: "nbf, not-before", + Usage: `The time before which the JWT must not be accepted. NOT_BEFORE must be a +numeric value representing a Unix timestamp. If not provided, the current time +is used.`, + }, + cli.Int64Flag{ + Name: "iat, issued-at", + Usage: `The time at which the JWT was issued, used to determine the age of the JWT. +ISSUED_AT must be a numeric value representing a Unix timestamp. If not +provided, the current time is used.`, + }, + cli.StringFlag{ + Name: "jti, jwt-id", + Usage: `A unique identifier for the JWT. The identifier must be assigned in a manner +that ensures that there is a negligible probability that the same value will +be accidentally assigned to multiple JWTs. The JTI claim can be used to +prevent a JWT from being replayed (i.e., recipient(s) can use JTI to make a +JWT one-time-use). The JTI argument is a case-sensitive string. If the '--jti' +flag is used without an argument a JTI will be generated randomly with +sufficient entropy to satisfy the collision-resistance criteria.`, + }, + cli.StringFlag{ + Name: "key", + Usage: `The key to use to sign the JWT. The KEY argument should be the name of a file. +JWTs can be signed using a private JWK (or a JWK encrypted as a JWE payload) +or a PEM encoded private key (or a private key encrypted using [TODO: insert +private key encryption mechanism]).`, + }, + cli.StringFlag{ + Name: "jwks", + Usage: `The JWK Set containing the key to use to sign the JWT. The JWKS argument +should be the name of a file. The file contents should be a JWK Set or a JWE +with a JWK Set payload. The '--jwks' flag requires the use of the '--kid' flag +to specify which key to use.`, + }, + cli.StringFlag{ + Name: "kid", + Usage: `The ID of the key used to sign the JWT. The KID argument is a case-sensitive +string. When used with '--jwk' the KID value must match the +"kid" member of the JWK. When used with '--jwks' (a JWK Set) the KID value must +match the "kid" member of one of the JWKs in the JWK Set.`, + }, + cli.BoolFlag{ + Name: "subtle", + Hidden: true, + }, + cli.BoolFlag{ + Name: "no-kid", + Hidden: true, + }, + }, + } +} + +func signAction(ctx *cli.Context) error { + var err error + var payload interface{} + + // Read payload if provided + args := ctx.Args() + switch len(args) { + case 0: + // empty extra payload + payload = make(map[string]interface{}) + case 1: + // read payload from file or stdin (-) + if payload, err = readPayload(args[0]); err != nil { + return err + } + default: + return errors.Errorf("unknown arguments %v", args[1:]) + } + + isSubtle := ctx.Bool("subtle") + alg := ctx.String("alg") + + // Validate key, jwks and kid + key := ctx.String("key") + jwks := ctx.String("jwks") + kid := ctx.String("kid") + switch { + case key == "" && jwks == "": + return errs.RequiredOrFlag(ctx, "key", "jwks") + case key != "" && jwks != "": + return errs.MutuallyExclusiveFlags(ctx, "key", "jwks") + case jwks != "" && kid == "": + return errs.RequiredWithFlag(ctx, "kid", "jwks") + } + + // Read key from --key or --jwks + var jwk *jose.JSONWebKey + switch { + case key != "": + jwk, err = jose.ParseKey(key, "sig", alg, kid, isSubtle) + case jwks != "": + jwk, err = jose.ParseKeySet(jwks, alg, kid, isSubtle) + default: + return errs.RequiredOrFlag(ctx, "key", "jwks") + } + if err != nil { + return err + } + + // Public keys cannot be used for signing + if jwk.IsPublic() { + return errors.New("cannot use a public key for signing") + } + + // Key "use" must be "sig" to use for signing + if jwk.Use != "sig" && jwk.Use != "" { + return errors.Errorf("invalid jwk use: found '%s', expecting 'sig' (signature)", jwk.Use) + } + + // At this moment jwk.Algorithm should have an alg from: + // * alg parameter + // * jwk or jwkset + // * guessed for ecdsa and Ed25519 keys + if jwk.Algorithm == "" { + return errors.New("flag '--alg' is required with the given key") + } + if err := jose.ValidateJWK(jwk); err != nil { + return err + } + + // Add claims + c := &jose.Claims{ + Issuer: ctx.String("iss"), + Subject: ctx.String("sub"), + Audience: ctx.StringSlice("aud"), + Expiry: jose.NumericDate(ctx.Int64("exp")), + NotBefore: jose.NumericDate(ctx.Int64("nbf")), + IssuedAt: jose.NumericDate(ctx.Int64("iat")), + ID: ctx.String("jti"), + } + now := time.Now() + if c.NotBefore == 0 { + c.NotBefore = jose.NewNumericDate(now) + } + if c.IssuedAt == 0 { + c.IssuedAt = jose.NewNumericDate(now) + } + if c.ID == "" && ctx.IsSet("jti") { + if c.ID, err = utils.RandHex(40); err != nil { + return errors.Wrap(err, "error creating random jti") + } + } + + // Validate recommended claims + if !isSubtle { + switch { + case len(c.Issuer) == 0: + return errors.New("flag '--iss' is required unless '--subtle' is used") + case len(c.Audience) == 0: + return errors.New("flag '--aud' is required unless '--subtle' is used") + case len(c.Subject) == 0: + return errors.New("flag '--sub' is required unless '--subtle' is used") + case c.Expiry == 0: + return errors.New("flag '--exp' is required unless '--subtle' is used") + case c.Expiry.Time().Before(time.Now()): + return errors.New("flag '--exp' must be in the future unless '--subtle' is used") + } + } + + // Sign + so := new(jose.SignerOptions) + so.WithType("JWT") + if !ctx.Bool("no-kid") && jwk.KeyID != "" { + so.WithHeader("kid", jwk.KeyID) + } + + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(jwk.Algorithm), + Key: jwk.Key, + }, so) + if err != nil { + return errors.Wrapf(err, "error creating JWT signer") + } + + // Some implementations only accept "aud" as a string. + // Using claim overwriting for this special case. + aud := make(map[string]interface{}) + if len(c.Audience) == 1 { + aud["aud"] = c.Audience[0] + } + + raw, err := jose.Signed(signer).Claims(c).Claims(aud).Claims(payload).CompactSerialize() + if err != nil { + return errors.Wrapf(err, "error serializing JWT") + } + + fmt.Println(raw) + return nil +} + +func readPayload(filename string) (interface{}, error) { + var r io.Reader + if filename == "-" { + r = os.Stdin + } else { + b, err := ioutil.ReadFile(filename) + if err != nil { + return nil, errs.FileError(err, filename) + } + r = bytes.NewReader(b) + } + + v := make(map[string]interface{}) + if err := json.NewDecoder(r).Decode(&v); err != nil { + if filename == "-" { + return nil, errors.Wrap(err, "error decoding JSON from STDIN") + } + return nil, errors.Wrapf(err, "error decoding JSON from %s", filename) + } + return v, nil +} diff --git a/command/crypto/jwt/verify.go b/command/crypto/jwt/verify.go new file mode 100644 index 00000000..bffa9db9 --- /dev/null +++ b/command/crypto/jwt/verify.go @@ -0,0 +1,231 @@ +package jwt + +import ( + "os" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/cli/command/crypto/internal/jose" + "github.com/smallstep/cli/command/crypto/internal/utils" + "github.com/smallstep/cli/errs" + "github.com/urfave/cli" +) + +func verifyCommand() cli.Command { + return cli.Command{ + Name: "verify", + Action: cli.ActionFunc(verifyAction), + Usage: "verify a signed JWT data structure and return the payload", + Description: `The 'step crypto jwt verify' command reads a JWT data structure from STDIN; +checks that the audience, issuer, and algorithm are in agreement with +expectations; verifies the digital signature or message authentication code as +appropriate; and outputs the decoded payload of the JWT on STDOUT. If +verification fails a non-zero failure code is returned. If verification +succeeds the command returns 0. + + For a JWT to be verified successfully: + * The JWT must be well formed (no errors during deserialization) + * The ALGORITHM must match the "alg" member in the JWT header + * The ISSUER and AUIENCE must match the "iss" and "aud" claims in the JWT, + respectively + * The KID must match the "kid" member in the JWT header (if both are + present) and must match the "kid" in the JWK or the "kid" of one of the + JWKs in JWKS + * The JWT signature must be successfully verified`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "iss, issuer", + Usage: `The issuer of this JWT. The ISSUER must match the value of the "iss" claim in +the JWT. ISSUER is a case-sensitive string.`, + }, + cli.StringFlag{ + Name: "aud, audience", + Usage: `The identity of the principal running this command. The AUDIENCE specified +must match one of the values in the "aud" claim, indicating the intended +recipient(s) of the JWT. AUDIENCE is a case-sensitive string.`, + }, + cli.StringFlag{ + Name: "alg, algorithm", + Usage: `The signature or MAC algorithm to use. Algorithms are case-sensitive strings +defined in RFC7518. If the KEY used do verify the JWT is not a JWK, or if it +is a JWK but does not have an "alg" member indicating its the intended +algorithm for use with the key, then the '--alg' flag is required to prevent +algorithm downgrade attacks. To disable this protection you can pass the +'--insecure' flag and omit the '--alg' flag.`, + }, + cli.StringFlag{ + Name: "key", + Usage: `The key to use to verify the JWS. The KEY argument should be the name of a +file. The contents of the file can be a public or private JWK (or a JWK +encrypted as a JWE payload) or a public or private PEM (or a private key +encrypted using.`, + }, + cli.StringFlag{ + Name: "jwks", + Usage: `The JWK Set containing the key to use to verify the JWS. The JWKS argument +should be the name of a file. The file contents should be a JWK Set or a JWE +with a JWK Set payload. The JWS being verified should have a "kid" member that +matches the "kid" of one of the JWKs in the JWK Set. If the JWS does not have +a "kid" member the '--kid' flag can be used.`, + }, + cli.StringFlag{ + Name: "kid", + Usage: `The ID of the key used to sign the JWK, used to select a JWK from a JWK Set. +The KID argument is a case-sensitive string. If the input JWS has a "kid" +member its value must match KID or verification will fail.`, + }, + cli.BoolFlag{ + Name: "subtle", + Hidden: true, + }, + cli.BoolFlag{ + Name: "no-exp-check", + Hidden: true, + }, + cli.BoolFlag{ + Name: "insecure", + Hidden: true, + }, + }, + } +} + +// Get the public key for a JWK. +func publicKey(jwk *jose.JSONWebKey) interface{} { + if jose.IsSymmetric(jwk) { + return jwk.Key + } + return jwk.Public().Key +} + +func verifyAction(ctx *cli.Context) error { + token, err := utils.ReadString(os.Stdin) + if err != nil { + return errors.Wrap(err, "error reading token") + } + + tok, err := jose.ParseSigned(token) + if err != nil { + return errors.Errorf("error parsing token: %s", strings.TrimPrefix(err.Error(), "square/go-jose: ")) + } + + // Validate key, jwks and kid + key := ctx.String("key") + jwks := ctx.String("jwks") + kid := ctx.String("kid") + alg := ctx.String("alg") + switch { + case key == "" && jwks == "": + return errs.RequiredOrFlag(ctx, "key", "jwks") + case key != "" && jwks != "": + return errs.MutuallyExclusiveFlags(ctx, "key", "jwks") + case jwks != "" && kid == "": + if tok.Headers[0].KeyID == "" { + return errs.RequiredWithFlag(ctx, "kid", "jwks") + } + kid = tok.Headers[0].KeyID + } + + // Validate subtled + isSubtle := ctx.Bool("subtle") + iss := ctx.String("iss") + aud := ctx.String("aud") + if !isSubtle { + switch { + case len(iss) == 0: + return errs.RequiredSubtleFlag(ctx, "iss") + case len(aud) == 0: + return errs.RequiredSubtleFlag(ctx, "aud") + } + } + + // Read key from --key or --jwks + var jwk *jose.JSONWebKey + switch { + case key != "": + jwk, err = jose.ParseKey(key, "sig", alg, kid, isSubtle) + case jwks != "": + jwk, err = jose.ParseKeySet(jwks, alg, kid, isSubtle) + default: + return errs.RequiredOrFlag(ctx, "key", "jwks") + } + if err != nil { + return err + } + + // At this moment jwk.Algorithm should have an alg from: + // * alg parameter + // * jwk or jwkset + // * guessed for ecdsa and ed25519 keys + if jwk.Algorithm == "" { + return errors.New("flag '--alg' is required with the given key") + } + if err := jose.ValidateJWK(jwk); err != nil { + return err + } + + // We don't support multiple signatures or any critical headers + if len(tok.Headers) > 1 { + return errors.New("validation failed: multiple signatures are not supported") + } + if _, ok := tok.Headers[0].ExtraHeaders["crit"]; ok { + return errors.New("validation failed: unrecognized critical headers (crit)") + } + if !isSubtle && alg != "" && tok.Headers[0].Algorithm != "" && alg != tok.Headers[0].Algorithm { + return errors.Errorf("alg %s does not match the alg on JWT (%s)", alg, tok.Headers[0].Algorithm) + } + + claims := jose.Claims{} + if err := tok.Claims(publicKey(jwk), &claims); err != nil { + switch err { + case jose.ErrCryptoFailure: + return errors.New("validation failed: invalid signature") + default: + return errors.Wrap(err, "claim verify failed") + } + } + + expected := jose.Expected{Issuer: iss} + if aud != "" { + expected.Audience = jose.Audience{aud} + } + if !ctx.Bool("no-exp-check") { + // TODO: The `go-jose` library makes it hard for us to differentiate + // between a JWT that has no "exp" paramater and one that has an "exp" + // paramater set to 0. We conflate the two cases here. This is + // definitely not correct as an explicit 0 should be rejected. + if claims.Expiry == 0 { + if !ctx.Bool("subtle") { + return errors.New(`jwt must have "exp" property unless '--subtle' is used`) + } + } else { + expected.Time = time.Now() + } + } else { + if !ctx.Bool("insecure") { + return errs.RequiredInsecureFlag(ctx, "no-exp-check") + } + } + + if err := claims.ValidateWithLeeway(expected, 0); err != nil { + switch err { + case jose.ErrInvalidIssuer: + return errors.New("validation failed: invalid issuer claim (iss)") + case jose.ErrInvalidAudience: + return errors.New("validation failed: invalid audience claim (aud)") + case jose.ErrNotValidYet: + return errors.New("validation failed: token not valid yet (nbf)") + case jose.ErrExpired: + return errors.Errorf("validation failed: token is expired by %s (exp)", expected.Time.Sub(claims.Expiry.Time()).Round(time.Millisecond)) + case jose.ErrInvalidSubject: // we're not currently checking the subject + return errors.New("validation failed: invalid subject subject (sub)") + case jose.ErrInvalidID: // we're not currently checking the id + return errors.New("validation failed: invalid ID claim (jti)") + default: + return errors.Wrap(err, "validation failed") + } + } + + return printToken(token) +} diff --git a/command/crypto/kdf/kdf.go b/command/crypto/kdf/kdf.go new file mode 100644 index 00000000..988fb555 --- /dev/null +++ b/command/crypto/kdf/kdf.go @@ -0,0 +1,240 @@ +package kdf + +import ( + "crypto/subtle" + "fmt" + + "github.com/pkg/errors" + "github.com/smallstep/cli/command/crypto/internal/utils" + "github.com/smallstep/cli/errs" + "github.com/urfave/cli" + + "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/scrypt" +) + +// Command returns the cli.Command for kdf and related subcommands. +func Command() cli.Command { + return cli.Command{ + Name: "kdf", + Usage: "key derivation functions for password hashing and verification", + UsageText: "step crypto kdf [SUBCOMMAND_FLAGS]", + Subcommands: cli.Commands{ + hashCommand(), + compareCommand(), + }, + } +} + +func hashCommand() cli.Command { + return cli.Command{ + Name: "hash", + Action: cli.ActionFunc(hashAction), + Usage: "derive a secret key from a secret value (e.g., a password)", + UsageText: "step crypto kdf hash [INPUT] [--alg ALGORITHM]", + Description: `The 'step crypto kdf hash' command uses a key derivation function (KDF) to +produce a pseudorandom secret key based on some (presumably secret) input +value. This is useful for password verification approaches based on password +hashing. Key derivation functions are designed to be computationally +intensive, making it more difficult for attackers to perform brute-force +attacks on password databases. + + If this command is run without the optional INPUT argument and STDIN is a +TTY (i.e., you're running the command in an interactive terminal and not +piping input to it) you'll be prompted to enter a value on STDERR. If STDIN is +not a TTY it will be read without prompting. + + This command will produce a string encoding of the KDF output along with the +algorithm used, salt, and any parameters required for validation in PHC string +format. + + The KDFs are run with parameters that are considered safe. The 'scrypt' +parameters are currently fixed at N=32768, r=8 and p=1. The 'bcrypt' work +factor is currently fixed at 10. + +POSITIONAL ARGUMENTS + + INPUT + The input to the key derivation function. INPUT is optional and its use is +not recommended. If this argument is provided the '--insecure' flag must also +be provided because your (presumably secret) INPUT will likely be logged and +appear in places you might not expect. If omitted input is read from STDIN. + `, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "alg", + Value: "scrypt", + Usage: `The KDF algorithm to use. + + ALGORITHM must be one of: + scrypt + A password-based KDF designed to use exponential time and memory. + bcrypt + A password-based KDF designed to use exponential time.`, + }, + cli.BoolFlag{ + Name: "insecure", + Hidden: true, + }, + }, + } +} + +func hashAction(ctx *cli.Context) error { + var err error + var input []byte + + // Get kdf method + var kdf func([]byte) (string, error) + switch alg := ctx.String("alg"); alg { + case "scrypt": + kdf = doScrypt + case "bcrypt": + kdf = doBcrypt + default: + return errs.InvalidFlagValue(ctx, "alg", alg, "") + } + + // Grab input from terminal or arguments + switch ctx.NArg() { + case 0: + input, err = utils.ReadInput("Enter password to hash: ") + if err != nil { + return err + } + case 1: + if !ctx.Bool("insecure") { + return errs.InsecureArgument(ctx, "INPUT") + } + input = []byte(ctx.Args().Get(0)) + default: + return errs.TooManyArguments(ctx) + } + + // Hash input + hash, err := kdf(input) + if err != nil { + return err + } + + fmt.Println(hash) + return nil +} + +// doScrypt uses scrypt-32768 to derive the given password. +func doScrypt(password []byte) (string, error) { + salt, err := phcGetSalt(16) + if err != nil { + return "", errors.Wrap(err, "error creating salt") + } + // use scrypt-32768 by default + p := scryptParams[scryptHash32768] + hash, err := scrypt.Key(password, salt, p.N, p.r, p.p, p.kl) + if err != nil { + return "", errors.Wrap(err, "error deriving password") + } + + return phcEncode("scrypt", p.getParams(), salt, hash), nil +} + +// doBcrypt uses bcrypt to derive the given password. +func doBcrypt(password []byte) (string, error) { + hash, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) + if err != nil { + return "", errors.Wrap(err, "error deriving password") + + } + return string(hash), nil +} + +func compareCommand() cli.Command { + return cli.Command{ + Name: "compare", + Action: cli.ActionFunc(compareAction), + Usage: "compare a plaintext value (e.g., a password) and a hash", + UsageText: "step crypto kdf compare PHC_HASH [INPUT]", + Description: `The 'step crypto kdf compare' command compares a plaintext value (e.g., a +password) with an existing KDF password hash in PHC string format. The PHC +string input indicates which KDF algorithm and parameters to use. + + If the input matches PHC_HASH the command prints a human readable message +indicating success to STDERR and returns 0. If the input does not match an +error will be printed to STDERR and the command will exit with a non-zero +return code. + + If this command is run without the optional INPUT argument and STDIN is a +TTY (i.e., you're running the command in an interactive terminal and not +piping input to it) you'll be prompted to enter a value on STDERR. If STDIN is +not a TTY it will be read without prompting. + +POSITIONAL ARGUMENTS + + INPUT + The plaintext value to compare with PHC_HASH. INPUT is optional and its +use is not recommended. If this argument is provided the '--insecure' flag +must also be provided because your (presumably secret) INPUT will likely be +logged and appear in places you might not expect. If omitted input is read +from STDIN.`, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "insecure", + Hidden: true, + }, + }, + } +} + +func compareAction(ctx *cli.Context) error { + var err error + var hashStr string + var input []byte + + switch ctx.NArg() { + case 0: + return errs.MissingArguments(ctx, "PHC_HASH") + case 1: + hashStr = ctx.Args().Get(0) + input, err = utils.ReadInput("Enter password to compare: ") + if err != nil { + return err + } + case 2: + if !ctx.Bool("insecure") { + return errs.InsecureArgument(ctx, "INPUT") + } + args := ctx.Args() + hashStr, input = args[0], []byte(args[1]) + default: + return errs.TooManyArguments(ctx) + } + + id, params, salt, hash, err := phcDecode(hashStr) + if err != nil { + return errors.Wrap(err, "error decoding hash") + } + + var valid bool + switch id { + case bcryptHash: + valid = (bcrypt.CompareHashAndPassword(hash, input) == nil) + case scryptHash: + p, err := newScryptParams(params) + if err != nil { + return err + } + hashedPass, err := scrypt.Key(input, salt, p.N, p.r, p.p, len(hash)) + if err != nil { + return errors.Wrap(err, "error deriving input") + } + valid = (subtle.ConstantTimeCompare(hash, hashedPass) == 1) + default: + return errors.Errorf("invalid or unsupported hash method with id '%s'", id) + } + + if valid { + fmt.Println("ok") + return nil + } + + return errors.New("fail") +} diff --git a/command/crypto/kdf/phc.go b/command/crypto/kdf/phc.go new file mode 100644 index 00000000..39e7f7c6 --- /dev/null +++ b/command/crypto/kdf/phc.go @@ -0,0 +1,106 @@ +package kdf + +import ( + "crypto/rand" + "encoding/base64" + "io" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +// phcEncoding is the alphabet used to encode/decode the hashes. It's based on +// the PHC string format: +// +// https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md +var phcEncoding = base64.RawStdEncoding + +// phcGetSalt is a helper that returns a random slice of n bytes. +func phcGetSalt(n int) ([]byte, error) { + salt := make([]byte, n) + if _, err := io.ReadFull(rand.Reader, salt); err != nil { + return nil, err + } + return salt, nil +} + +// phcAtoi returns the number in the string value or n if value is empty. +func phcAtoi(value string, n int) (int, error) { + if value == "" { + return n, nil + } + return strconv.Atoi(value) +} + +// phcParamsToMap parses the parameters in the string s and returns them in a +// map of keys and values. +func phcParamsToMap(s string) map[string]string { + parameters := strings.Split(s, ",") + m := make(map[string]string, len(parameters)) + for _, p := range parameters { + subs := strings.SplitN(p, "=", 2) + if len(subs) == 2 { + m[subs[0]] = subs[1] + } else { + m[subs[0]] = "" + } + } + return m +} + +// phcEncode creates a string using the PHC format. +func phcEncode(identifier, params string, salt, hash []byte) string { + ret := "$" + identifier + if len(params) > 0 { + ret += "$" + params + } + if len(salt) > 0 { + ret += "$" + phcEncoding.EncodeToString(salt) + } + if len(hash) > 0 { + ret += "$" + phcEncoding.EncodeToString(hash) + } + return ret +} + +// phcDecode returns the different parts of a PHC encoded string. +func phcDecode(s string) (id string, params string, salt []byte, hash []byte, err error) { + subs := strings.SplitN(s, "$", 5) + if subs[0] != "" || len(subs) < 2 || (subs[1] == bcryptHash && len(subs) != 4) { + return "", "", nil, nil, errors.Errorf("cannot decode password hash %s", s) + } + + // Special case for bcrypt + // return just the id and the full hash + if subs[1] == bcryptHash { + return bcryptHash, "", nil, []byte(s), nil + } + + switch len(subs) { + case 5: // id + params + salt + hash + if hash, err = phcEncoding.DecodeString(subs[4]); err != nil { + return "", "", nil, nil, err + } + if salt, err = phcEncoding.DecodeString(subs[3]); err != nil { + return "", "", nil, nil, err + } + id, params = subs[1], subs[2] + case 4: // id + salt + hash + if hash, err = phcEncoding.DecodeString(subs[3]); err != nil { + return "", "", nil, nil, err + } + if salt, err = phcEncoding.DecodeString(subs[2]); err != nil { + return "", "", nil, nil, err + } + id = subs[1] + case 3: // id + params + id, params = subs[1], subs[2] + case 2: // id + id = subs[1] + default: + return "", "", nil, nil, errors.Errorf("cannot decode password hash %s", s) + } + + return +} diff --git a/command/crypto/kdf/scrypt.go b/command/crypto/kdf/scrypt.go new file mode 100644 index 00000000..3d13bb2b --- /dev/null +++ b/command/crypto/kdf/scrypt.go @@ -0,0 +1,62 @@ +package kdf + +import ( + "fmt" + "math" + + "github.com/pkg/errors" +) + +const ( + bcryptHash = "2a" + scryptHash = "scrypt" + scryptHash16384 = "scrypt-16384" + scryptHash32768 = "scrypt-32768" + scryptHash65536 = "scrypt-65536" +) + +var scryptParams = map[string]scryptParam{ + scryptHash16384: {16384, 8, 1, 32}, + scryptHash32768: {32768, 8, 1, 32}, + scryptHash65536: {65536, 8, 1, 32}, +} + +type scryptParam struct { + N, r, p int + kl int +} + +func newScryptParams(s string) (*scryptParam, error) { + sp := new(scryptParam) + params := phcParamsToMap(s) + + if ln, err := phcAtoi(params["ln"], 16); err != nil { + return nil, err + } else if ln < 1 { + return nil, errors.Errorf("invalid scrypt parameter ln=%s", params["ln"]) + } else { + sp.N = int(math.Pow(2, float64(ln))) + } + + if r, err := phcAtoi(params["r"], 8); err != nil { + return nil, err + } else if r < 1 { + return nil, errors.Errorf("invalid scrypt parameter r=%s", params["r"]) + } else { + sp.r = r + } + + if p, err := phcAtoi(params["p"], 1); err != nil { + return nil, err + } else if p < 1 { + return nil, errors.Errorf("invalid scrypt parameter p=%s", params["p"]) + } else { + sp.p = p + } + + return sp, nil +} + +func (s *scryptParam) getParams() string { + return fmt.Sprintf("ln=%d,r=%d,p=%d", int(math.Log2(float64(s.N))), s.r, s.p) +} diff --git a/command/crypto/keypair.go b/command/crypto/keypair.go new file mode 100644 index 00000000..ce064c6b --- /dev/null +++ b/command/crypto/keypair.go @@ -0,0 +1,146 @@ +package crypto + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/smallstep/cli/command/crypto/internal/utils" + "github.com/smallstep/cli/crypto/keys" + "github.com/smallstep/cli/errs" + "github.com/smallstep/cli/utils/reader" + "github.com/urfave/cli" +) + +func createKeyPairCommand() cli.Command { + return cli.Command{ + Name: "keypair", + Action: cli.ActionFunc(createAction), + Usage: "generate a public /private keypair in PEM format.", + UsageText: `step crypto keypair PUB_FILE PRIV_FILE [--type=TYPE] [--size=SIZE] [--curve=CURVE]`, + Description: `The 'step crypto keypair' command generates a raw public / private keypair + in PEM format. These keys can be used by other operations to sign + and encrypt data, and the public key can be bound to an identity in a CSR and + signed by a CA to produce a certificate. + + Private keys are encrypted using a password. You'll be prompted for this password + automatically when the key is used. + +POSITIONAL ARGUMENTS: + PUB_FILE + The path to write the public key. + + PRIV_FILE + The path to write the private key.`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "type", + Value: "EC", + Usage: `The type of key to generate. + + TYPE is a case-sensitive string and must be one of: + EC + Generate an asymmetric Elliptic Curve Key Pair. + RSA + Generate an asymmetric RSA (Rivest–Shamir–Adleman) Key Pair. + OKP + Generate an asymmetric Octet Key Pair.`, + }, + cli.IntFlag{ + Name: "size", + Usage: `The size (in bits) of the key for RSA and oct key types. RSA keys require a + minimum key size of 2048 bits.`, + Value: 2048, + }, + cli.StringFlag{ + Name: "crv, curve", + Value: "P-256", + Usage: `The elliptic curve to use for this keypair for EC and OKP key types. + + CURVE is a case-sensitive string and must be one of: + P-256 + NIST P-256 Curve; compatible with 'EC' key type only + P-384 + NIST P-384 Curve; compatible with 'EC' key type only + P-521 + NIST P-521 Curve; compatible with 'EC' key type only + Ed25519 + EdDSA Curve 25519; compatible with 'OKP' key type only`, + }, + cli.BoolFlag{ + Name: "insecure", + Hidden: true, + }, + cli.BoolFlag{ + Name: "no-password", + Usage: `The directive to leave the private key unencrypted. This is not recommended.`, + }, + }, + } +} + +func createAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 2); err != nil { + return err + } + + pubFile := ctx.Args().Get(0) + privFile := ctx.Args().Get(1) + if pubFile == privFile { + return errs.EqualArguments(ctx, "PUB_FILE", "PRIV_FILE") + } + + typ := ctx.String("type") + crv := ctx.String("crv") + if ctx.IsSet("crv") { + switch typ { + case "EC", "OKP": + default: + return errors.Errorf("key type '%s' is not compatible with flag '--crv'", typ) + } + } else { + switch typ { + // If crv not set and the key type is OKP then set cruve Ed25519. + // The cli assumes a default curve for EC key type. + case "OKP": + crv = "Ed25519" + } + } + if ctx.IsSet("size") && typ != "RSA" { + return errors.Errorf("key type '%s' is not compatible with flag '--size'", typ) + } + size := ctx.Int("size") + insecure := ctx.Bool("insecure") + noPass := ctx.Bool("no-password") + + if noPass && !insecure { + return errs.RequiredWithFlag(ctx, "insecure", "no-password") + } + if size < 2048 && !insecure { + return errs.MinSizeInsecureFlag(ctx, "size", "2048") + } + if size <= 0 { + return errs.MinSizeFlag(ctx, "size", "0") + } + + pub, priv, err := keys.GenerateKeyPair(typ, crv, size) + if err != nil { + return errors.WithStack(err) + } + + if err := utils.WritePublicKey(pub, pubFile); err != nil { + return errors.WithStack(err) + } + + var pass string + if !noPass { + reader.ReadPasswordSubtle( + fmt.Sprintf("Password with which to encrypt private key file `%s`: ", privFile), + &pass, "Password", reader.RetryOnEmpty) + + } + if err := utils.WritePrivateKey(priv, pass, privFile); err != nil { + return errors.WithStack(err) + } + + return nil +} diff --git a/command/crypto/nacl/auth.go b/command/crypto/nacl/auth.go new file mode 100644 index 00000000..16c987c9 --- /dev/null +++ b/command/crypto/nacl/auth.go @@ -0,0 +1,109 @@ +package nacl + +import ( + "encoding/hex" + "fmt" + "io/ioutil" + "os" + + "github.com/smallstep/cli/errs" + + "github.com/smallstep/cli/command/crypto/internal/utils" + "golang.org/x/crypto/nacl/auth" + + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +func authCommand() cli.Command { + return cli.Command{ + Name: "auth", + Usage: "authenticates a message using a secret key", + UsageText: "step crypto nacl auth SUBCOMMAND [SUBCOMMAND_FLAGS]", + Subcommands: cli.Commands{ + authDigestCommand(), + authVerifyCommand(), + }, + } +} + +func authDigestCommand() cli.Command { + return cli.Command{ + Name: "digest", + Action: cli.ActionFunc(authDigestAction), + Usage: "generates a 32-byte digest for a message", + UsageText: "step crypto nacl auth digest KEY_FILE", + } +} + +func authVerifyCommand() cli.Command { + return cli.Command{ + Name: "verify", + Action: cli.ActionFunc(authVerifyAction), + Usage: "checks digest is a valid for a message", + UsageText: "step crypto nacl auth verify KEY_FILE DIGEST", + } +} + +func authDigestAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 1); err != nil { + return err + } + + keyFile := ctx.Args().Get(0) + + key, err := ioutil.ReadFile(keyFile) + if err != nil { + return errs.FileError(err, keyFile) + } else if len(key) != auth.KeySize { + return errors.Errorf("invalid key file: key size is not %d bytes", auth.KeySize) + } + + input, err := utils.ReadAll(os.Stdin) + if err != nil { + return errs.Wrap(err, "error reading from STDIN") + } + + var k [32]byte + copy(k[:], key) + + sum := auth.Sum(input, &k) + fmt.Println(hex.EncodeToString(sum[:])) + return nil +} + +func authVerifyAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 2); err != nil { + return err + } + + args := ctx.Args() + keyFile, digest := args[0], args[1] + + key, err := ioutil.ReadFile(keyFile) + if err != nil { + return errs.FileError(err, keyFile) + } else if len(key) != auth.KeySize { + return errors.Errorf("invalid key file: key size is not %d bytes", auth.KeySize) + } + + sum, err := hex.DecodeString(digest) + if err != nil { + return errors.Wrap(err, "error decoding digest") + } + + input, err := utils.ReadAll(os.Stdin) + if err != nil { + return errs.Wrap(err, "error reading from STDIN") + } + + var k [32]byte + copy(k[:], key) + + if auth.Verify(sum, input, &k) { + fmt.Println("ok") + return nil + } + + return errors.New("fail") +} diff --git a/command/crypto/nacl/box.go b/command/crypto/nacl/box.go new file mode 100644 index 00000000..3e9cc592 --- /dev/null +++ b/command/crypto/nacl/box.go @@ -0,0 +1,243 @@ +package nacl + +import ( + "crypto/rand" + "fmt" + "io/ioutil" + "os" + + "github.com/smallstep/cli/errs" + + "golang.org/x/crypto/nacl/box" + + "github.com/pkg/errors" + "github.com/smallstep/cli/command/crypto/internal/utils" + "github.com/urfave/cli" +) + +func boxCommand() cli.Command { + return cli.Command{ + Name: "box", + Usage: "authenticate and encrypt small messages using public-key cryptography", + UsageText: "step crypto nacl box SUBCOMMAND [SUBCOMMAND_FLAGS]", + Subcommands: cli.Commands{ + boxKeypairCommand(), + boxOpenCommand(), + boxSealCommand(), + }, + } +} + +func boxKeypairCommand() cli.Command { + return cli.Command{ + Name: "keypair", + Action: cli.ActionFunc(boxKeypairAction), + Usage: "generate a key for use with seal and open", + UsageText: "step crypto nacl box keypair PUB_FILE PRIV_FILE", + Description: `Generates a new public/private keypair suitable for use with seal and open. +The private key is encrypted using a password in a nacl secretbox. + +POSITIONAL ARGUMENTS + + PUB_FILE + The path to write the public key. + + PRIV_FILE + The path to write the encrypted private key.`, + } +} + +func boxKeypairAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 2); err != nil { + return err + } + + args := ctx.Args() + pubFile, privFile := args[0], args[1] + if pubFile == privFile { + return errs.EqualArguments(ctx, "PUB_FILE", "PRIV_FILE") + } + + pub, priv, err := box.GenerateKey(rand.Reader) + if err != nil { + return errors.Wrap(err, "error generating key") + } + + if err := utils.WriteFile(pubFile, pub[:], 0600); err != nil { + return errs.FileError(err, pubFile) + } + + if err := utils.WriteFile(privFile, priv[:], 0600); err != nil { + return errs.FileError(err, privFile) + } + + return nil +} + +func boxOpenCommand() cli.Command { + return cli.Command{ + Name: "open", + Action: cli.ActionFunc(boxOpenAction), + Usage: "authenticate and decrypt a box produced by seal", + UsageText: "step crypto nacl box open NONCE SENDER_PUB_KEY PRIV_KEY [--raw]", + Description: `Authenticate and decrypt a box produced by seal using the specified KEY. If +PRIV_KEY is encrypted you will be prompted for the password. The sealed box is +read from STDIN and the decrypted plaintext is written to STDOUT. + +POSITIONAL ARGUMENTS + + NONCE + The nonce provided when the box was sealed. + + SENDER_PUB_KEY + The path to the public key of the peer that produced the sealed box. + + PRIV_KEY + The path to the private key used to open the box.`, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "raw", + Usage: "Indicates that input is not base64 encoded", + }, + }, + } +} + +func boxOpenAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 3); err != nil { + return err + } + + args := ctx.Args() + nonce, pubFile, privFile := []byte(args[0]), args[1], args[2] + + if len(nonce) > 24 { + return errors.New("nonce cannot be longer than 24 bytes") + } + + pub, err := ioutil.ReadFile(pubFile) + if err != nil { + return errs.FileError(err, pubFile) + } else if len(pub) != 32 { + return errors.New("invalid public key: key size is not 32 bytes") + } + + priv, err := ioutil.ReadFile(privFile) + if err != nil { + return errs.FileError(err, privFile) + } else if len(priv) != 32 { + return errors.New("invalid private key: key size is not 32 bytes") + } + + input, err := utils.ReadAll(os.Stdin) + if err != nil { + return errs.Wrap(err, "error reading input") + } + + var rawInput []byte + if ctx.Bool("raw") { + rawInput = input + } else { + // DecodeLen returns the maximum length, + // Decode will return the actual length. + rawInput = make([]byte, b64Encoder.DecodedLen(len(input))) + n, err := b64Encoder.Decode(rawInput, input) + if err != nil { + return errors.Wrap(err, "error decoding base64 input") + } + rawInput = rawInput[:n] + } + + var n [24]byte + var pb, pv [32]byte + copy(n[:], nonce) + copy(pb[:], pub) + copy(pv[:], priv) + + // Fixme: if we prepend the nonce in the seal we can use use rawInput[24:] + // as the message and rawInput[:24] as the nonce instead of requiring one. + raw, ok := box.Open(nil, rawInput, &n, &pb, &pv) + if !ok { + return errors.New("error authenticating or decrypting input") + } + + os.Stdout.Write(raw) + return nil +} + +func boxSealCommand() cli.Command { + return cli.Command{ + Name: "seal", + Action: cli.ActionFunc(boxSealAction), + Usage: "produce an authenticated and encrypted ciphertext", + UsageText: "step crypto nacl box seal NONCE RECIPIENT_PUB_KEY PRIV_KEY [--raw]", + Description: `Reads plaintext from STDIN and writes an encrypted and authenticated +ciphertext to STDOUT. The "box" can be open by the a recipient who has access +to the private key corresponding to RECIPIENT_PUB_KEY. + +POSITIONAL ARGUMENTS + + NONCE + Must be unique for each distinct message for a given pair of keys. + + RECIPIENT_PUB_KEY + The path to the public key of the intended recipient of the sealed box. + + PRIV_KEY + The path to the private key used for authentication.`, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "raw", + Usage: "Do not base64 encode output", + }, + }, + } +} + +func boxSealAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 3); err != nil { + return err + } + + args := ctx.Args() + nonce, pubFile, privFile := []byte(args[0]), args[1], args[2] + + if len(nonce) > 24 { + return errors.New("nonce cannot be longer than 24 bytes") + } + + pub, err := ioutil.ReadFile(pubFile) + if err != nil { + return errs.FileError(err, pubFile) + } else if len(pub) != 32 { + return errors.New("invalid public key: key size is not 32 bytes") + } + + priv, err := ioutil.ReadFile(privFile) + if err != nil { + return errs.FileError(err, privFile) + } else if len(priv) != 32 { + return errors.New("invalid private key: key size is not 32 bytes") + } + + input, err := utils.ReadInput("Write text to seal: ") + if err != nil { + return errors.Wrap(err, "error reading input") + } + + var n [24]byte + var pb, pv [32]byte + copy(n[:], nonce) + copy(pb[:], pub) + copy(pv[:], priv) + + // Fixme: we can prepend nonce[:] so it's not necessary in the open. + raw := box.Seal(nil, input, &n, &pb, &pv) + if ctx.Bool("raw") { + os.Stdout.Write(raw) + } else { + fmt.Println(b64Encoder.EncodeToString(raw)) + } + + return nil +} diff --git a/command/crypto/nacl/nacl.go b/command/crypto/nacl/nacl.go new file mode 100644 index 00000000..0399e437 --- /dev/null +++ b/command/crypto/nacl/nacl.go @@ -0,0 +1,47 @@ +package nacl + +import ( + "encoding/base64" + + "github.com/urfave/cli" +) + +// Command returns the cli.Command for nacl and related subcommands. +func Command() cli.Command { + return cli.Command{ + Name: "nacl", + Usage: "easy-to-use high-speed tools for encryption and signing", + UsageText: "step crypto nacl SUBCOMMAND [SUBCOMMAND_FLAGS]", + Description: `The 'step crypto nacl' command group is a thin CLI wrapper around the NaCl +(pronounced "salt") cryptography library. NaCl's goal is to provide all of the +core operations needed to build higher-level cryptographic tools. + + Perhaps its biggest advantage is simplicity. NaCl was designed to be easy to +use and hard to misuse. Typical cryptographic libraries force you to specify +choices for cryptographic primitives and constructions (e.g., sign this +message with 4096-bit RSA using PKCS#1 v2.0 with SHA-256). But most people are +not cryptographers. These choices become foot guns. By contrast, NaCl allows +you to simply say "sign this message". NaCl ships with a preselected choice -- +a state-of-the-art signature system suitable for most applications -- and it +has a side mechanism through which a cryptographer can easily override the +choice of signature system. + + There are language bindings and pure implementations of NaCl for all major +languages. For internal use cases where compatibility with open standards like +JWT are not an issue, NaCl should be your default choice for cryptographic +needs. + + TODO: Are we NaCl or libsodium compliant? Maybe have a flag at this level to +decide? The golang default is libsodium compatible. I think all that changes +are the defaults for the 'box' operations -- NaCl doesn't support Curve25519 +yet.`, + Subcommands: cli.Commands{ + authCommand(), + boxCommand(), + secretboxCommand(), + signCommand(), + }, + } +} + +var b64Encoder = base64.RawURLEncoding diff --git a/command/crypto/nacl/secretbox.go b/command/crypto/nacl/secretbox.go new file mode 100644 index 00000000..e2388121 --- /dev/null +++ b/command/crypto/nacl/secretbox.go @@ -0,0 +1,153 @@ +package nacl + +import ( + "fmt" + "io/ioutil" + "os" + + "golang.org/x/crypto/nacl/secretbox" + + "github.com/pkg/errors" + "github.com/smallstep/cli/command/crypto/internal/utils" + "github.com/smallstep/cli/errs" + "github.com/urfave/cli" +) + +func secretboxCommand() cli.Command { + return cli.Command{ + Name: "secretbox", + Usage: "encrypts and authenticates small messages using secret-key cryptography", + UsageText: "step crypto nacl secretbox SUBCOMMAND [SUBCOMMAND_FLAGS]", + Description: `TODO`, + Subcommands: cli.Commands{ + secretboxOpenCommand(), + secretboxSealCommand(), + }, + } +} + +func secretboxOpenCommand() cli.Command { + return cli.Command{ + Name: "open", + Action: cli.ActionFunc(secretboxOpenAction), + Usage: "authenticates and decrypts a box produced by seal", + UsageText: "step crypto nacl secretbox open NONCE KEY_FILE [--raw]", + Description: `TODO`, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "raw", + Usage: "Indicates that input is not base64 encoded", + }, + }, + } +} + +func secretboxOpenAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 2); err != nil { + return err + } + + args := ctx.Args() + nonce, keyFile := []byte(args[0]), args[1] + + if len(nonce) > 24 { + return errors.New("nonce cannot be longer than 24 bytes") + } + + key, err := ioutil.ReadFile(keyFile) + if err != nil { + return errs.FileError(err, keyFile) + } else if len(key) != 32 { + return errors.New("invalid key file: key size is not 32 bytes") + } + + input, err := utils.ReadAll(os.Stdin) + if err != nil { + return errors.Wrap(err, "error reading input") + } + + var rawInput []byte + if ctx.Bool("raw") { + rawInput = input + } else { + // DecodeLen returns the maximum length, + // Decode will return the actual length. + rawInput = make([]byte, b64Encoder.DecodedLen(len(input))) + n, err := b64Encoder.Decode(rawInput, input) + if err != nil { + return errors.Wrap(err, "error decoding base64 input") + } + rawInput = rawInput[:n] + } + + var n [24]byte + var k [32]byte + copy(n[:], nonce) + copy(k[:], key) + + // Fixme: if we prepend the nonce in the seal we can use use rawInput[24:] + // as the message and rawInput[:24] as the nonce instead of requiring one. + raw, ok := secretbox.Open(nil, rawInput, &n, &k) + if !ok { + return errors.New("error authenticating or decrypting input") + } + + os.Stdout.Write(raw) + return nil +} + +func secretboxSealCommand() cli.Command { + return cli.Command{ + Name: "seal", + Action: cli.ActionFunc(secretboxSealAction), + Usage: "produces an encrypted ciphertext", + UsageText: "step crypto nacl secretbox seal NONCE KEY_FILE [--raw]", + Description: `TODO`, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "raw", + Usage: "Do not base64 encode output", + }, + }, + } +} + +func secretboxSealAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 2); err != nil { + return err + } + + args := ctx.Args() + nonce, keyFile := []byte(args[0]), args[1] + + if len(nonce) > 24 { + return errors.New("nonce cannot be longer than 24 bytes") + } + + key, err := ioutil.ReadFile(keyFile) + if err != nil { + return errs.FileError(err, keyFile) + } else if len(key) != 32 { + return errors.New("invalid key: key size is not 32 bytes") + } + + input, err := utils.ReadInput("Write text to seal: ") + if err != nil { + return errors.Wrap(err, "error reading input") + } + + var n [24]byte + var k [32]byte + copy(n[:], nonce) + copy(k[:], key) + + // Fixme: we can prepend nonce[:] so it's not necessary in the open. + raw := secretbox.Seal(nil, input, &n, &k) + if ctx.Bool("raw") { + os.Stdout.Write(raw) + } else { + fmt.Println(b64Encoder.EncodeToString(raw)) + } + + return nil +} diff --git a/command/crypto/nacl/sign.go b/command/crypto/nacl/sign.go new file mode 100644 index 00000000..7170d34c --- /dev/null +++ b/command/crypto/nacl/sign.go @@ -0,0 +1,169 @@ +package nacl + +import ( + "crypto/rand" + "fmt" + "io/ioutil" + "os" + + "golang.org/x/crypto/nacl/sign" + + "github.com/pkg/errors" + "github.com/smallstep/cli/command/crypto/internal/utils" + "github.com/smallstep/cli/errs" + "github.com/urfave/cli" +) + +func signCommand() cli.Command { + return cli.Command{ + Name: "sign", + Usage: "signs small messages using public-key cryptography", + UsageText: "step crypto nacl sign SUBCOMMAND [SUBCOMMAND_FLAGS]", + Subcommands: cli.Commands{ + signKeypairCommand(), + signOpenCommand(), + signSignCommand(), + }, + } +} + +func signKeypairCommand() cli.Command { + return cli.Command{ + Name: "keypair", + Action: cli.ActionFunc(signKeypairAction), + Usage: "generates a pair for use with sign and open", + UsageText: "step crypto nacl sign keypair PUB_FILE PRIV_FILE", + } +} + +func signOpenCommand() cli.Command { + return cli.Command{ + Name: "open", + Action: cli.ActionFunc(signOpenAction), + Usage: "verifies a signed message produced by sign", + UsageText: "step crypto nacl sign open PUB_FILE", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "raw", + Usage: "Indicates that input is not base64 encoded", + }, + }, + } +} + +func signSignCommand() cli.Command { + return cli.Command{ + Name: "sign", + Action: cli.ActionFunc(signSignAction), + Usage: "signs a message using Ed25519", + UsageText: "step crypto nacl sign sign PRIV_FILE", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "raw", + Usage: "Do not base64 encode output", + }, + }, + } +} + +func signKeypairAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 2); err != nil { + return err + } + + args := ctx.Args() + pubFile, privFile := args[0], args[1] + if pubFile == privFile { + return errs.EqualArguments(ctx, "PUB_FILE", "PRIV_FILE") + } + + pub, priv, err := sign.GenerateKey(rand.Reader) + if err != nil { + return errors.Wrap(err, "error generating key") + } + + if err := utils.WriteFile(pubFile, pub[:], 0600); err != nil { + return errs.FileError(err, pubFile) + } + + if err := utils.WriteFile(privFile, priv[:], 0600); err != nil { + return errs.FileError(err, privFile) + } + + return nil +} + +func signOpenAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 1); err != nil { + return err + } + + pubFile := ctx.Args().Get(0) + pub, err := ioutil.ReadFile(pubFile) + if err != nil { + return errs.FileError(err, pubFile) + } else if len(pub) != 32 { + return errors.New("invalid public key: key size is not 32 bytes") + } + + input, err := utils.ReadAll(os.Stdin) + if err != nil { + return errors.Wrap(err, "error reading input") + } + + var rawInput []byte + if ctx.Bool("raw") { + rawInput = input + } else { + // DecodeLen returns the maximum length, + // Decode will return the actual length. + rawInput = make([]byte, b64Encoder.DecodedLen(len(input))) + n, err := b64Encoder.Decode(rawInput, input) + if err != nil { + return errors.Wrap(err, "error decoding base64 input") + } + rawInput = rawInput[:n] + } + + var pb [32]byte + copy(pb[:], pub) + + raw, ok := sign.Open(nil, rawInput, &pb) + if !ok { + return errors.New("error authenticating input") + } + + os.Stdout.Write(raw) + return nil +} + +func signSignAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 1); err != nil { + return err + } + + privFile := ctx.Args().Get(0) + priv, err := ioutil.ReadFile(privFile) + if err != nil { + return errs.FileError(err, privFile) + } else if len(priv) != 64 { + return errors.New("invalid private key: key size is not 64 bytes") + } + + input, err := utils.ReadInput("Write text to sign: ") + if err != nil { + return errors.Wrap(err, "error reading input") + } + + var pv [64]byte + copy(pv[:], priv) + + raw := sign.Sign(nil, input, &pv) + if ctx.Bool("raw") { + os.Stdout.Write(raw) + } else { + fmt.Println(b64Encoder.EncodeToString(raw)) + } + + return nil +} diff --git a/command/crypto/otp/generate.go b/command/crypto/otp/generate.go new file mode 100644 index 00000000..c3538f2f --- /dev/null +++ b/command/crypto/otp/generate.go @@ -0,0 +1,100 @@ +package otp + +import ( + "bytes" + "fmt" + "image/png" + + "github.com/smallstep/cli/command/crypto/internal/utils" + "github.com/smallstep/cli/errs" + "github.com/urfave/cli" +) + +func generateCommand() cli.Command { + return cli.Command{ + Name: "generate", + Action: cli.ActionFunc(generateAction), + Usage: "one-time password", + UsageText: `step crypto otp generate`, + Description: `The 'step crypto otp generate' command does TOTP and HTOP`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "issuer, iss", + Usage: `Name of the issuing organization (e.g., smallstep.com)`, + }, + cli.StringFlag{ + Name: "account", + Usage: `Name of the user's account (e.g., a username or email +address)`, + }, + cli.IntFlag{ + Name: "period", + Usage: `Number of seconds a TOTP hash is valid. Defaults to 30 +seconds.`, + Value: 30, + }, + cli.IntFlag{ + Name: "length, digits", + Usage: `Length of one-time passwords. Defaults to 6.`, + Value: 6, + }, + cli.IntFlag{ + Name: "secret-size", + Usage: `Size of generated TOTP secret. Defaults to 20.`, + Value: 20, + }, + cli.StringFlag{ + Name: "alg, algorithm", + Usage: `Algorithm to use for HMAC. Defaults to SHA1. Must be +one of: SHA1, SHA256, SHA512`, + Value: "SHA1", + }, + cli.BoolFlag{ + Name: "url", + Usage: `Output a TOTP Key URI. See +https://github.com/google/google-authenticator/wiki/Key-Uri-Format`, + }, + cli.StringFlag{ + Name: "qr", + Usage: `Write a QR code to the specified path`, + }, + }, + } +} + +func generateAction(ctx *cli.Context) error { + switch { + case len(ctx.String("issuer")) == 0: + return errs.RequiredFlag(ctx, "issuer") + case len(ctx.String("account")) == 0: + return errs.RequiredFlag(ctx, "account") + } + + key, err := generate(ctx) + if err != nil { + return err + } + + if ctx.IsSet("qr") { + filename := ctx.String("qr") + + // Convert TOTP key into a PNG + var buf bytes.Buffer + img, err := key.Image(200, 200) + if err != nil { + return err + } + png.Encode(&buf, img) + if err := utils.WriteFile(filename, buf.Bytes(), 0644); err != nil { + return errs.FileError(err, filename) + } + } + + if ctx.Bool("url") { + fmt.Println(key.String()) + } else { + fmt.Println(key.Secret()) + } + + return nil +} diff --git a/command/crypto/otp/otp.go b/command/crypto/otp/otp.go new file mode 100644 index 00000000..90ec6ea4 --- /dev/null +++ b/command/crypto/otp/otp.go @@ -0,0 +1,52 @@ +package otp + +import ( + "strings" + + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" + "github.com/smallstep/cli/errs" + "github.com/urfave/cli" +) + +// Command returns the cli.Command for jwt and related subcommands. +func Command() cli.Command { + return cli.Command{ + Name: "otp", + Usage: "generate and verify one-time passwords", + UsageText: "step crypto otp SUBCOMMAND [SUBCOMMAND_ARGUMENTS] [GLOBAL_FLAGS] [SUBCOMMAND_FLAGS]", + Description: `Implements TOTP and HOTP one-time passwords (mention RFCs)`, + Subcommands: cli.Commands{ + generateCommand(), + verifyCommand(), + }, + } +} + +func algFromString(ctx *cli.Context, alg string) (otp.Algorithm, error) { + switch strings.ToUpper(alg) { + case "SHA1": + return otp.AlgorithmSHA1, nil + case "SHA256": + return otp.AlgorithmSHA256, nil + case "SHA512": + return otp.AlgorithmSHA512, nil + default: + return 0, errs.InvalidFlagValue(ctx, "alg", alg, "SHA1, SHA256, or SHA512") + } +} + +func generate(ctx *cli.Context) (*otp.Key, error) { + alg, err := algFromString(ctx, ctx.String("alg")) + if err != nil { + return nil, err + } + return totp.Generate(totp.GenerateOpts{ + Issuer: ctx.String("issuer"), + AccountName: ctx.String("account"), + Period: uint(ctx.Int("period")), + SecretSize: uint(ctx.Int("secret-size")), + Digits: otp.Digits(ctx.Int("length")), + Algorithm: alg, + }) +} diff --git a/command/crypto/otp/verify.go b/command/crypto/otp/verify.go new file mode 100644 index 00000000..0543ec08 --- /dev/null +++ b/command/crypto/otp/verify.go @@ -0,0 +1,96 @@ +package otp + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" + "github.com/smallstep/cli/errs" + "github.com/urfave/cli" +) + +func verifyCommand() cli.Command { + return cli.Command{ + Name: "verify", + Action: cli.ActionFunc(verifyAction), + Usage: "one-time password", + UsageText: `step crypto otp verify`, + Description: `The 'step crypto otp verify' command does TOTP and HTOP`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "secret", + Usage: `A file containing the OTP secret`, + }, + cli.IntFlag{ + Name: "period", + Usage: `Number of seconds a TOTP hash is valid. Defaults to 30 +seconds.`, + Value: 30, + }, + cli.IntFlag{ + Name: "skew", + Usage: `Periods before or after current time to allow. Defaults +to 0. Values greater than 1 require '--insecure'`, + Value: 0, + }, + cli.IntFlag{ + Name: "length digits", + Usage: `Length of one-time passwords. Defaults to 6.`, + Value: 6, + }, + cli.StringFlag{ + Name: "alg algorithm", + Usage: `Algorithm to use for HMAC. Defaults to SHA1. Must be +one of: SHA1, SHA256, SHA512`, + Value: "SHA1", + }, + cli.IntFlag{ + Name: "time", + Usage: `Time to use for TOTP calculation. Defaults to now.`, + }, + }, + } +} + +func promptForPasscode() string { + reader := bufio.NewReader(os.Stdin) + fmt.Print("Enter Passcode: ") + text, _ := reader.ReadString('\n') + return text +} + +func verifyAction(ctx *cli.Context) error { + filename := ctx.String("secret") + if len(filename) == 0 { + return errs.RequiredFlag(ctx, "secret") + } + + b, err := ioutil.ReadFile(filename) + if err != nil { + return errs.FileError(err, filename) + } + secret := string(b) + + if strings.HasPrefix(secret, "otpauth://") { + url, err := otp.NewKeyFromURL(secret) + if err != nil { + return err + } + secret = url.Secret() + } + + passcode := promptForPasscode() + valid := totp.Validate(passcode, secret) + if valid { + fmt.Println("ok") + os.Exit(0) + } else { + fmt.Println("fail") + os.Exit(1) + } + return nil +} diff --git a/command/oauth/cmd.go b/command/oauth/cmd.go new file mode 100644 index 00000000..be8043f1 --- /dev/null +++ b/command/oauth/cmd.go @@ -0,0 +1,640 @@ +package oauth + +import ( + "bufio" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/urfave/cli" + + "github.com/smallstep/cli/command" + "github.com/smallstep/cli/crypto" + "github.com/smallstep/cli/errs" + "github.com/smallstep/cli/exec" + jose "gopkg.in/square/go-jose.v2" + "gopkg.in/square/go-jose.v2/jwt" +) + +// These are the OAuth2.0 client IDs from the Step CLI. This application is +// using the OAuth2.0 flow for installed applications described on +// https://developers.google.com/identity/protocols/OAuth2InstalledApp +// +// The Step CLI app and these client IDs do not have any APIs or services are +// enabled and it should be only used for OAuth 2.0 authorization. +// +// Due to the fact that the app cannot keep the client_secret confidential, +// incremental authorization with installed apps are not supported by Google. +// +// Google is also distributing the client ID and secret on the cloud SDK +// available here https://cloud.google.com/sdk/docs/quickstarts +const ( + defaultClientID = "1087160488420-8qt7bavg3qesdhs6it824mhnfgcfe8il.apps.googleusercontent.com" + defaultClientNotSoSecret = "udTrOT3gzrO7W9fDPgZQLfYJ" + + // The URN for getting verification token offline + oobCallbackUrn = "urn:ietf:wg:oauth:2.0:oob" + // The URN for token request grant type jwt-bearer + jwtBearerUrn = "urn:ietf:params:oauth:grant-type:jwt-bearer" +) + +type token struct { + AccessToken string `json:"access_token"` + IDToken string `json:"id_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` + Err string `json:"error,omitempty"` + ErrDesc string `json:"error_description,omitempty"` +} + +func init() { + cmd := cli.Command{ + Name: "oauth", + Usage: "Authenticate to Smallstep using OAuth OIDC", + UsageText: ` +**step oauth** [**--provider**=] [**--client-id**= **--client-secret**=] + [**--scope**= ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]] + +**step oauth** **--authorization-endpoint**= **--token-endpoint**= + **--client-id**= **--client-secret**= [**--scope**= ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]] + +**step oauth** [**--account**=] [**--authorization-endpoint**= **--token-endpoint**=] + [**--scope**= ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]] + +**step oauth** **--account**= **--jwt** [**--scope**= ...] [**--header**] [**-bare**] +`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "provider, idp", + Usage: "OAuth provider for authentication", + Value: "google", + }, + cli.StringFlag{ + Name: "email, e", + Usage: "Email to authenticate", + }, + cli.BoolFlag{ + Name: "console, c", + Usage: "Complete the flow while remaining only inside the terminal", + }, + cli.StringFlag{ + Name: "client-id", + Usage: "OAuth Client ID", + }, + cli.StringFlag{ + Name: "client-secret", + Usage: "OAuth Client Secret", + }, + cli.StringFlag{ + Name: "account", + Usage: "JSON file containing account details", + }, + cli.StringFlag{ + Name: "authorization-endpoint", + Usage: "OAuth Authorization Endpoint", + }, + cli.StringFlag{ + Name: "token-endpoint", + Usage: "OAuth Token Endpoint", + }, + cli.BoolFlag{ + Name: "header", + Usage: "Output HTTP Authorization Header (suitable for use with curl)", + }, + cli.BoolFlag{ + Name: "oidc", + Usage: "Output OIDC Token instead of OAuth Access Token", + }, + cli.BoolFlag{ + Name: "bare", + Usage: "Only output the token", + }, + cli.StringSliceFlag{ + Name: "scope", + Usage: "OAuth scopes", + }, + cli.BoolFlag{ + Name: "jwt", + Usage: "Generate a JWT Auth token instead of an OAuth Token (only works with service accounts)", + }, + }, + Action: oauthCmd, + } + + command.Register(cmd) +} + +func oauthCmd(c *cli.Context) error { + opts := &options{ + Provider: c.String("provider"), + Email: c.String("email"), + Console: c.Bool("console"), + } + if err := opts.Validate(); err != nil { + return errs.UsageExitError(c, err) + } + if (opts.Provider != "google" || c.IsSet("authorization-endpoint")) && !c.IsSet("client-id") { + return errors.New("flag '--client-id' required with '--provider'") + } + + clientID := defaultClientID + clientSecret := defaultClientNotSoSecret + if c.IsSet("client-id") { + clientID = c.String("client-id") + clientSecret = c.String("client-secret") + } + + authzEp := "" + tokenEp := "" + if c.IsSet("authorization-endpoint") { + if !c.IsSet("token-endpoint") { + return errors.New("flag '--authorization-endpoint' requires flag '--token-endpoint'") + } + opts.Provider = "" + authzEp = c.String("authorization-endpoint") + tokenEp = c.String("token-endpoint") + } + + do2lo := false + issuer := "" + // This code supports Google service accounts. Probably maybe also support JWKs? + if c.IsSet("account") { + opts.Provider = "" + filename := c.String("account") + b, err := ioutil.ReadFile(filename) + if err != nil { + return errors.Wrapf(err, "error reading account from %s", filename) + } + account := make(map[string]interface{}) + if err := json.Unmarshal(b, &account); err != nil { + return errors.Wrapf(err, "error reading %s: unsupported format", filename) + } + + if _, ok := account["installed"]; ok { + details := account["installed"].(map[string]interface{}) + authzEp = details["auth_uri"].(string) + tokenEp = details["token_uri"].(string) + clientID = details["client_id"].(string) + clientSecret = details["client_secret"].(string) + } else if accountType, ok := account["type"]; ok && "service_account" == accountType { + authzEp = account["auth_uri"].(string) + tokenEp = account["token_uri"].(string) + clientID = account["private_key_id"].(string) + clientSecret = account["private_key"].(string) + issuer = account["client_email"].(string) + do2lo = true + } else { + return errors.Wrapf(err, "error reading %s: unsupported account type", filename) + } + } + + scope := "openid email" + if c.IsSet("scope") { + scope = strings.Join(c.StringSlice("scope"), " ") + } + + o, err := newOauth(opts.Provider, clientID, clientSecret, authzEp, tokenEp, scope, opts.Email) + if err != nil { + return errs.ToError(err) + } + + var tok *token + if do2lo { + if c.Bool("jwt") { + tok, err = o.DoJWTAuthorization(issuer, scope) + } else { + tok, err = o.DoTwoLeggedAuthorization(issuer) + } + } else if opts.Console { + tok, err = o.DoManualAuthorization() + } else { + tok, err = o.DoLoopbackAuthorization() + } + + if err != nil { + return errs.ToError(err) + } + + if c.Bool("header") { + if c.Bool("oidc") { + fmt.Println("Authorization: Bearer", tok.IDToken) + } else { + fmt.Println("Authorization: Bearer", tok.AccessToken) + } + } else { + if c.Bool("bare") { + if c.Bool("oidc") { + fmt.Println(tok.IDToken) + } else { + fmt.Println(tok.AccessToken) + } + } else { + b, err := json.MarshalIndent(tok, "", " ") + if err != nil { + return errors.Wrapf(err, "error marshaling token data") + } + fmt.Println(string(b)) + } + } + + return nil +} + +type options struct { + Provider string + Email string + Console bool +} + +// Validate validates the options. +func (o *options) Validate() error { + if o.Provider != "google" && !strings.HasPrefix(o.Provider, "https://") { + return errors.New("Use a valid provider: google") + } + return nil +} + +type oauth struct { + provider string + clientID string + clientSecret string + scope string + loginHint string + redirectURI string + tokenEndpoint string + authzEndpoint string + userInfoEndpoint string // For testing + state string + codeChallenge string + errCh chan error + tokCh chan *token +} + +func newOauth(provider, clientID, clientSecret, authzEp, tokenEp, scope, loginHint string) (*oauth, error) { + state, err := crypto.GenerateRandomRestrictedString(32) + if err != nil { + return nil, err + } + + challenge, err := crypto.GenerateRandomRestrictedString(64) + if err != nil { + return nil, err + } + + switch provider { + case "google": + return &oauth{ + provider: provider, + clientID: clientID, + clientSecret: clientSecret, + scope: scope, + authzEndpoint: "https://accounts.google.com/o/oauth2/v2/auth", + tokenEndpoint: "https://www.googleapis.com/oauth2/v4/token", + userInfoEndpoint: "https://www.googleapis.com/oauth2/v3/userinfo", + loginHint: loginHint, + state: state, + codeChallenge: challenge, + errCh: make(chan error), + tokCh: make(chan *token), + }, nil + default: + userinfoEp := "" + if authzEp == "" && tokenEp == "" { + d, err := disco(provider) + if err != nil { + return nil, err + } + + if _, ok := d["authorization_endpoint"]; !ok { + return nil, errors.New("missing 'authorization_endpoint' in provider metadata") + } + if _, ok := d["token_endpoint"]; !ok { + return nil, errors.New("missing 'token_endpoint' in provider metadata") + } + authzEp = d["authorization_endpoint"].(string) + tokenEp = d["token_endpoint"].(string) + userinfoEp = d["token_endpoint"].(string) + } + return &oauth{ + provider: provider, + clientID: clientID, + clientSecret: clientSecret, + scope: scope, + authzEndpoint: authzEp, + tokenEndpoint: tokenEp, + userInfoEndpoint: userinfoEp, + loginHint: loginHint, + state: state, + codeChallenge: challenge, + errCh: make(chan error), + tokCh: make(chan *token), + }, nil + } +} + +func disco(provider string) (map[string]interface{}, error) { + url, err := url.Parse(provider) + if err != nil { + return nil, err + } + // TODO: OIDC and OAuth specify two different ways of constructing this + // URL. This is the OIDC way. Probably want to try both. See + // https://tools.ietf.org/html/rfc8414#section-5 + url.Path = path.Join(url.Path, "/.well-known/openid-configuration") + resp, err := http.Get(url.String()) + if err != nil { + return nil, errors.Wrapf(err, "error retrieving %s", url.String()) + } + defer resp.Body.Close() + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrapf(err, "error retrieving %s", url.String()) + } + details := make(map[string]interface{}) + if err := json.Unmarshal(b, &details); err != nil { + return nil, errors.Wrapf(err, "error reading %s: unsupported format", url.String()) + } + return details, err +} + +// DoLoopbackAuthorization performs the log in into the identity provider +// opening a browser and using a redirect_uri in a loopback IP address +// (http://127.0.0.1:port or http://[::1]:port). +func (o *oauth) DoLoopbackAuthorization() (*token, error) { + srv := httptest.NewServer(o) + o.redirectURI = srv.URL + defer srv.Close() + + // Get auth url and open it in a browser + authURL, err := o.Auth() + if err != nil { + return nil, err + } + + if err := exec.OpenInBrowser(authURL); err != nil { + fmt.Fprintln(os.Stderr, "Cannot open a web browser on your platform.") + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, "Open a local web browser and visit:") + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, authURL) + fmt.Fprintln(os.Stderr) + } else { + fmt.Fprintln(os.Stderr, "Your default web browser has been opened to visit:") + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, authURL) + fmt.Fprintln(os.Stderr) + } + + // Wait for response and return the token + select { + case tok := <-o.tokCh: + return tok, nil + case err := <-o.errCh: + return nil, err + case <-time.After(2 * time.Minute): + return nil, errors.New("oauth command timed out, please try again") + } +} + +// DoManualAuthorization performs the log in into the identity provider +// allowing the user to open a browser on a different system and then entering +// the authorization code on the Step CLI. +func (o *oauth) DoManualAuthorization() (*token, error) { + o.redirectURI = oobCallbackUrn + authURL, err := o.Auth() + if err != nil { + return nil, err + } + + fmt.Fprintln(os.Stderr, "Open a local web browser and visit:") + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, authURL) + fmt.Fprintln(os.Stderr) + + // Read from the command line + fmt.Fprint(os.Stderr, "Enter verification code: ") + reader := bufio.NewReader(os.Stdin) + code, err := reader.ReadString('\n') + if err != nil { + return nil, errors.WithStack(err) + } + + tok, err := o.Exchange(o.tokenEndpoint, code) + if err != nil { + return nil, err + } + if tok.Err != "" || tok.ErrDesc != "" { + return nil, errors.Errorf("Error exchanging authorization code: %s. %s", tok.Err, tok.ErrDesc) + } + return tok, nil +} + +// DoTwoLeggedAuthorization performs two-legged OAuth using the jwt-bearer +// grant type. +func (o *oauth) DoTwoLeggedAuthorization(issuer string) (*token, error) { + pemBytes := []byte(o.clientSecret) + block, _ := pem.Decode(pemBytes) + if block == nil { + return nil, fmt.Errorf("failed to read private key pem block") + } + priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, errors.Wrap(err, "error parsing private key") + } + + // Add claims + now := int(time.Now().Unix()) + c := map[string]interface{}{ + "aud": o.tokenEndpoint, + "nbf": now, + "iat": now, + "exp": now + 3600, + "iss": issuer, + "scope": o.scope, + } + + so := new(jose.SignerOptions) + so.WithType("JWT") + so.WithHeader("kid", o.clientID) + + // Sign JWT + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: "RS256", + Key: priv, + }, so) + if err != nil { + return nil, errors.Wrapf(err, "error creating JWT signer") + } + + raw, err := jwt.Signed(signer).Claims(c).CompactSerialize() + if err != nil { + return nil, errors.Wrapf(err, "error serializing JWT") + } + + // Construct the POST request to fetch the OAuth token. + params := url.Values{ + "assertion": []string{string(raw)}, + "grant_type": []string{jwtBearerUrn}, + } + + // Send the POST request and return token. + resp, err := http.PostForm(o.tokenEndpoint, params) + if err != nil { + return nil, errors.Wrapf(err, "error from token endpoint") + } + defer resp.Body.Close() + + var tok token + if err := json.NewDecoder(resp.Body).Decode(&tok); err != nil { + return nil, errors.WithStack(err) + } + + return &tok, nil +} + +// DoJWTAuthorization generates a JWT instead of an OAuth token. Only works for +// certain APIs. See https://developers.google.com/identity/protocols/OAuth2ServiceAccount#jwt-auth. +func (o *oauth) DoJWTAuthorization(issuer, aud string) (*token, error) { + pemBytes := []byte(o.clientSecret) + block, _ := pem.Decode(pemBytes) + if block == nil { + return nil, fmt.Errorf("failed to read private key pem block") + } + priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, errors.Wrap(err, "error parsing private key") + } + + // Add claims + now := int(time.Now().Unix()) + c := map[string]interface{}{ + "aud": aud, + "nbf": now, + "iat": now, + "exp": now + 3600, + "iss": issuer, + "sub": issuer, + } + + so := new(jose.SignerOptions) + so.WithType("JWT") + so.WithHeader("kid", o.clientID) + + // Sign JWT + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: "RS256", + Key: priv, + }, so) + if err != nil { + return nil, errors.Wrapf(err, "error creating JWT signer") + } + + raw, err := jwt.Signed(signer).Claims(c).CompactSerialize() + if err != nil { + return nil, errors.Wrapf(err, "error serializing JWT") + } + + tok := &token{string(raw), "", "", 3600, "Bearer", "", ""} + return tok, nil +} + +// ServeHTTP is the handler that performs the OAuth 2.0 dance and returns the +// tokens using channels. +func (o *oauth) ServeHTTP(w http.ResponseWriter, req *http.Request) { + q := req.URL.Query() + errStr := q.Get("error") + if errStr != "" { + o.badRequest(w, "Failed to authenticate: "+errStr) + return + } + + code := q.Get("code") + if code == "" { + o.badRequest(w, "Failed to authenticate: missing or invalid code") + return + } + + state := q.Get("state") + if state == "" || state != o.state { + o.badRequest(w, "Failed to authenticate: missing or invalid state") + return + } + + tok, err := o.Exchange(o.tokenEndpoint, code) + if err != nil { + o.badRequest(w, "Failed exchanging authorization code: "+err.Error()) + } + if tok.Err != "" || tok.ErrDesc != "" { + o.badRequest(w, fmt.Sprintf("Failed exchanging authorization code: %s. %s", tok.Err, tok.ErrDesc)) + return + } + + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "text/plain; charset=utf-8") + w.Write([]byte("Success: look for the token on the command line")) + o.tokCh <- tok +} + +// Auth returns the OAuth 2.0 authentication url. +func (o *oauth) Auth() (string, error) { + u, err := url.Parse(o.authzEndpoint) + if err != nil { + return "", errors.WithStack(err) + } + + q := u.Query() + q.Add("client_id", o.clientID) + q.Add("redirect_uri", o.redirectURI) + q.Add("response_type", "code") + q.Add("scope", o.scope) + q.Add("state", o.state) + q.Add("code_challenge_method", "plain") + q.Add("code_challenge", o.codeChallenge) + if o.loginHint != "" { + q.Add("login_hint", o.loginHint) + } + u.RawQuery = q.Encode() + return u.String(), nil +} + +// Exchange exchanges the authorization code for refresh and access tokens. +func (o *oauth) Exchange(tokenEndpoint, code string) (*token, error) { + data := url.Values{} + data.Set("code", code) + data.Set("client_id", o.clientID) + data.Set("client_secret", o.clientSecret) + data.Set("redirect_uri", o.redirectURI) + data.Set("grant_type", "authorization_code") + data.Set("code_verifier", o.codeChallenge) + + resp, err := http.PostForm(tokenEndpoint, data) + if err != nil { + return nil, errors.WithStack(err) + } + defer resp.Body.Close() + + var tok token + if err := json.NewDecoder(resp.Body).Decode(&tok); err != nil { + return nil, errors.WithStack(err) + } + + return &tok, nil +} + +func (o *oauth) badRequest(w http.ResponseWriter, msg string) { + w.WriteHeader(http.StatusBadRequest) + w.Header().Add("Content-Type", "text/plain; charset=utf-8") + w.Write([]byte(msg)) + o.errCh <- errors.New(msg) +} diff --git a/command/version/version.go b/command/version/version.go new file mode 100644 index 00000000..ccea3897 --- /dev/null +++ b/command/version/version.go @@ -0,0 +1,27 @@ +package version + +import ( + "fmt" + + "github.com/urfave/cli" + + "github.com/smallstep/cli/command" + "github.com/smallstep/cli/config" +) + +func init() { + cmd := cli.Command{ + Name: "version", + Usage: "Displays the current version of the cli", + Action: Command, + } + + command.Register(cmd) +} + +// Command prints out the current version of the tool +func Command(c *cli.Context) error { + fmt.Printf("%s\n", config.Version()) + fmt.Printf("Release Date: %s\n", config.ReleaseDate()) + return nil +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 00000000..e9487d79 --- /dev/null +++ b/config/config.go @@ -0,0 +1,86 @@ +package config + +import ( + "fmt" + "log" + "os" + "os/user" + "path" + "runtime" + "time" +) + +// version and buildTime are filled in during build by the Makefile +var ( + buildTime = "N/A" + commit = "N/A" +) + +// StepPathEnv defines the name of the environment variable that can overwrite +// the default configuration path. +const StepPathEnv = "STEPPATH" + +// stepPath will be populated in init() with the proper STEPPATH. +var stepPath string + +// StepPath returns the path for the step configuration directory, this is +// defined by the environment variable STEPPATH or if this is not set it will +// default to '$HOME/.step'. +func StepPath() string { + return stepPath +} + +func init() { + l := log.New(os.Stderr, "", 0) + + // Get step path from environment or user's home directory + stepPath = os.Getenv(StepPathEnv) + if stepPath == "" { + usr, err := user.Current() + if err != nil || usr.HomeDir == "" { + l.Fatalf("Error obtaining home directory, please define environment variable %s.", StepPathEnv) + } + stepPath = path.Join(usr.HomeDir, ".step") + } + + // Check for presence or create it if necessary + if fi, err := os.Stat(stepPath); err != nil { + if err := os.MkdirAll(stepPath, 0700); err != nil { + if e, ok := err.(*os.PathError); ok { + err = e.Err + } + l.Fatalf("Error creating '%s': %s.", stepPath, err) + } + } else if !fi.IsDir() { + l.Fatalf("File '%s' is not a directory.", stepPath) + } + // cleanup + stepPath = path.Clean(stepPath) +} + +// Set updates the Version and ReleaseDate +func Set(v, t string) { + buildTime = t + commit = v +} + +// Version returns the current version of the binary +func Version() string { + out := commit + if commit == "N/A" { + out = "0000000-dev" + } + + return fmt.Sprintf("Smallstep CLI/%s (%s/%s)", + out, runtime.GOOS, runtime.GOARCH) +} + +// ReleaseDate returns the time of when the binary was built +func ReleaseDate() string { + out := buildTime + if buildTime == "N/A" { + out = time.Now().UTC().Format("2006-01-02 15:04 MST") + } + + return out +} diff --git a/crypto/certificates/x509/certTemplate.go b/crypto/certificates/x509/certTemplate.go new file mode 100644 index 00000000..bb416068 --- /dev/null +++ b/crypto/certificates/x509/certTemplate.go @@ -0,0 +1,450 @@ +package x509 + +import ( + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "net" + "strings" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/pkg/errors" +) + +const ( + defaultDuration = time.Hour * 24 * 365 +) + +var ( + // DefaultCertValidity is the minimum validity of an end-entity (not root or intermediate) certificate. + DefaultCertValidity = 24 * time.Hour + // DefaultRootCertValidity is the default validity of a root certificate in the step PKI. + DefaultRootCertValidity = time.Hour * 24 * 365 * 10 + // DefaultIntermediateCertValidity is the default validity of a root certificate in the step PKI. + DefaultIntermediateCertValidity = time.Hour * 24 * 365 * 10 + + // TLS Options + + // DefaultTLSMinVersion default minimum version of TLS. + DefaultTLSMinVersion = TLSVersion(1.2) + // DefaultTLSMaxVersion default maximum version of TLS. + DefaultTLSMaxVersion = TLSVersion(1.2) + // DefaultTLSRenegotiation default TLS connection renegotiation policy. + DefaultTLSRenegotiation = false // Never regnegotiate. + // DefaultTLSCipherSuites specifies default step ciphersuite(s). + DefaultTLSCipherSuites = CipherSuites{ + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + } + // ApprovedTLSCipherSuites smallstep approved ciphersuites. + ApprovedTLSCipherSuites = CipherSuites{ + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", + } +) + +// PkixName allows us to add our own methods to pkix.Name +type PkixName pkix.Name + +// CertTemplate allows us to add our own methods to x509.Certificate +type CertTemplate x509.Certificate + +// PkixNameBuilder for organizing pkix fields. +type PkixNameBuilder struct { + Country, Organization, OrganizationalUnit *string + Locality, Province, StreetAddress, PostalCode *string + SerialNumber, CommonName *string +} + +// Now is a helper function that returns the current time with the location +// set to UTC. +func Now() time.Time { + return time.Now().UTC() +} + +// Country generates a function that modifies the Country value +// of a certificate name struct. +// Takes a pointer to a comma separated string of countries +// (e.g. " ecuador,italy,brazil") +// Returns a function that will modify, in-place, a CertTemplate. +func Country(countries string) func(*PkixName) error { + return func(pn *PkixName) error { + if countries == "" { + return errors.Errorf("countries cannot be empty") + } + // appends countries to existing list + for _, c := range strings.Split(countries, ",") { + pn.Country = append(pn.Country, c) + } + return nil + } +} + +// Locality generates a function that modifies the Country value +// of a certificate name struct. +// Takes a pointer to a comma separated string of localities +// (e.g. " ecuador,italy,brazil") +// Returns a function that will modify, in-place, a CertTemplate. +func Locality(localities string) func(*PkixName) error { + return func(pn *PkixName) error { + if localities == "" { + return errors.Errorf("localities cannot be empty") + } + // appends localities to existing list + for _, l := range strings.Split(localities, ",") { + pn.Locality = append(pn.Locality, l) + } + return nil + } +} + +// CommonName generates a function that modifies the CommonName value +// of a certificate name struct. +// Takes a pointer to a common name string. +// Returns a function that will modify, in-place, a CertTemplate. +func CommonName(common string) func(*PkixName) error { + return func(pn *PkixName) error { + if common == "" { + return errors.Errorf("common cannot be empty") + } + pn.CommonName = common + return nil + } +} + +// Organization generates a function that modifies the Organization value +// of a certificate name struct. +// Takes a pointer to a comma separated string of organizations +// (e.g. " ecuador,italy,brazil") +// Returns a function that will modify, in-place, a CertTemplate. +func Organization(orgs string) func(*PkixName) error { + return func(pn *PkixName) error { + if orgs == "" { + return errors.Errorf("orgs cannot be empty") + } + // appends organizations to existing list + for _, o := range strings.Split(orgs, ",") { + pn.Organization = append(pn.Organization, o) + } + return nil + } +} + +// NewPkixName generates a new PkixName struct. +// Takes an arbitrary number of augmenting functions each of which modifies +// a PkixName. A default PkixName is created and then the optional +// augmenter functions are applied one after another in the order in which they +// appear as parameters. +// Returns the address of a new PkixName and an error object that will be +// nil on success or contain error data on failure. +func NewPkixName(options ...func(*PkixName) error) (*PkixName, error) { + pn := &PkixName{} + + for _, op := range options { + err := op(pn) + if err != nil { + return nil, err + } + } + + return pn, nil +} + +// Hosts generates a function that modifies the IPAddresses and DNSNames values +// of a certificate. +// Takes a pointer to a comma separated string of hostnames +// (e.g. "127.0.0.1,smallstep.com,blog.smallstep.com") +// Returns a function that will modify, in-place, a CertTemplate. +func Hosts(hosts string) func(*CertTemplate) error { + return func(ct *CertTemplate) error { + if hosts == "" { + return errors.New("hosts cannot be empty") + } + hostsL := strings.Split(hosts, ",") + for _, h := range hostsL { + if h == "" { + continue + } else if ip := net.ParseIP(h); ip != nil { + ct.IPAddresses = append(ct.IPAddresses, ip) + } else { + ct.DNSNames = append(ct.DNSNames, h) + } + } + return nil + } +} + +// NotBeforeAfter generates a function that modifies the NotBefore and NotAfter +// values of a certificate. +// Takes a pair of arguments used to compute the window of time during which +// the certificate should be valid. +// Returns a function that will modify, in-place, a CertTemplate. +func NotBeforeAfter(from time.Time, duration time.Duration) func(*CertTemplate) error { + return func(ct *CertTemplate) error { + if from.IsZero() { + ct.NotBefore = Now() + } else { + ct.NotBefore = from + } + + switch { + case duration < 0: + return errors.New("Duration must be greater than 0") + case duration == 0: + ct.NotAfter = ct.NotBefore.Add(defaultDuration) + default: + ct.NotAfter = ct.NotBefore.Add(duration) + } + + return nil + } +} + +// SerialNumber generates a function that modifies the SerialNumber value of +// a CertTemplate. +// Takes an argument that will be used to set the SerialNumber. +// Returns a function that will modify, in-place, a CertTemplate. +func SerialNumber(sn *string) func(*CertTemplate) error { + return func(ct *CertTemplate) error { + // If sn is empty then generate a random serial number. + if sn == nil || len(*sn) == 0 { + return errors.Errorf("SerialNumber cannot be nil or empty") + } + + ct.SerialNumber = new(big.Int) + ct.SerialNumber.SetString(*sn, 10) + if _, succ := ct.SerialNumber.SetString(*sn, 10); !succ { + return errors.Errorf("Failed to parse serial number: %s", + *sn) + } + return nil + } +} + +// Issuer generates a function that modifies the Issuer value of +// a CertTemplate. +// Takes an argument that will be used to populate the Issuer pkix.Name. +// Returns a function that will modify, in-place, a CertTemplate. +func Issuer(pn PkixName) func(*CertTemplate) error { + return func(ct *CertTemplate) error { + ct.Issuer = pkix.Name(pn) + return nil + } +} + +// Subject generates a function that modifies the Subject value of +// a CertTemplate. +// Takes an argument that will be used to populate the Subject pkix.Name. +// Returns a function that will modify, in-place, a CertTemplate. +func Subject(pn PkixName) func(*CertTemplate) error { + return func(ct *CertTemplate) error { + ct.Subject = pkix.Name(pn) + return nil + } +} + +// CRLSign generates a function that modifies the KeyUsage bitmap value of a +// CertTemplate. +func CRLSign(c bool) func(*CertTemplate) error { + return func(ct *CertTemplate) error { + if c { + ct.KeyUsage |= x509.KeyUsageCRLSign + } else { + ct.KeyUsage &= ^(x509.KeyUsageCRLSign) + } + return nil + } +} + +// BasicConstraints generates a function that modifies the BasicConstraintsValid, +// IsCA, MaxPathLen, and MaxPathLenZero fields of a CertTemplate. +// +// If BasicConstraintsValid==true then the next two fields are valid. +// MaxPathLenZero indicates that BasicConstraintsValid==true and +// MaxPathLen==0 should be interpreted as an actual maximum path length +// of zero. Otherwise, that combination is interpreted as MaxPathLen +// not being set. +func BasicConstraints(bcv bool, isCA bool, maxPathLen int) func(*CertTemplate) error { + return func(ct *CertTemplate) error { + if maxPathLen < 0 { + return errors.Errorf("MaxPathLen must be >= 0") + } + if bcv { + ct.BasicConstraintsValid = true + ct.isCAHelper(isCA) + ct.MaxPathLen = maxPathLen + if maxPathLen == 0 { + ct.MaxPathLenZero = true + } + } else { + if isCA { + return errors.Errorf("isCA must be `false` if `BasicConstraintsValid==false`") + } + if maxPathLen != 0 { + return errors.Errorf("maxPathLen should be set to 0 if `BasicConstraintsValid==false`") + } + ct.BasicConstraintsValid = false + ct.isCAHelper(false) + ct.MaxPathLen = 0 + ct.MaxPathLenZero = false + } + return nil + } +} + +func (ct *CertTemplate) isCAHelper(isCA bool) { + if isCA { + ct.IsCA = true + ct.KeyUsage |= x509.KeyUsageCertSign + } else { + ct.IsCA = false + ct.KeyUsage &= ^(x509.KeyUsageCertSign) + } +} + +// ExtKeyUsage overwrites the extended key usage slice of a CertTemplate +func ExtKeyUsage(eku []x509.ExtKeyUsage) func(*CertTemplate) error { + return func(ct *CertTemplate) error { + ct.ExtKeyUsage = eku + return nil + } +} + +// NewCertTemplate generates and returns a new CertTemplate struct. +// Takes an arbitrary number of augmenting functions each of which modifies +// a CertTemplate. A default CertTemplate is created and then the optional +// augmenter functions are applied one after another in the order in which they +// were submitted. +// Returns the address of a new CertTemplate and an error object which will +// the nil on success and contain the reason and location of the failure. +func NewCertTemplate(options ...func(*CertTemplate) error) (*CertTemplate, error) { + var err error + notBefore := Now() + + ct := &CertTemplate{ + IsCA: false, + NotBefore: notBefore, + NotAfter: notBefore.Add(defaultDuration), + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + BasicConstraintsValid: false, + MaxPathLen: 0, + MaxPathLenZero: false, + } + + for _, op := range options { + err = op(ct) + if err != nil { + return nil, err + } + } + + if ct.SerialNumber == nil { + // TODO figure out how to test rand w/out threading as another arg + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + ct.SerialNumber, err = rand.Int(rand.Reader, serialNumberLimit) + // TODO error condition untested -- hard to test w/o mocking rand + if err != nil { + return nil, errors.Wrap(err, "Failed to generate serial number") + } + } + + return ct, nil +} + +// Compare compares the calling CertTemplate to the one provided as an argument. +// Returns nil if the two are equal, otherwise returns an error describing the diff. +// NOTE: this method avoids comparing a number of fields that are inconvenient or difficult +// to compare for equality. Check the `IgnoreFields` call below to check if the +// field you would like to check is being ignored. +func (ct CertTemplate) Compare(other CertTemplate) error { + var diff string + + if diff = cmp.Diff(CertTemplate(ct), other, + cmpopts.IgnoreFields(ct, "Extensions", "Issuer.Names", + "NotBefore", "NotAfter", "PublicKey", "Raw", "RawIssuer", + "RawSubject", "RawSubjectPublicKeyInfo", "RawTBSCertificate", + "SerialNumber", "Signature", "Subject.Names")); len(diff) != 0 { + return errors.Errorf("data mismatch -- %s", diff) + } + + if other.NotBefore.Before(ct.NotBefore.Add(-time.Second*10)) || + ct.NotBefore.After(ct.NotBefore.Add(time.Second*10)) { + return errors.Errorf("NotBefore mismatch -- expected: `%s`, but got: `%s`", + ct.NotBefore, other.NotBefore) + } + if ct.NotAfter.Before(other.NotAfter.Add(-time.Second*10)) || + ct.NotAfter.After(other.NotAfter.Add(time.Second*10)) { + return errors.Errorf("NotAfter mismatch -- expected: `%s`, but got: `%s`", + ct.NotAfter, other.NotAfter) + } + return nil +} + +// MergeASN1DN fills empty fields of a pkix.Name with default ASN1DN settings. +// If the field is already set (with non-empty value) then do not overwrite +// with default value, otherwise overwrite. +// TODO: test +func MergeASN1DN(n *pkix.Name, asn1dn *ASN1DN) error { + if n == nil || asn1dn == nil { + return errors.New("both arguments to mergeASN1DN must be non-nil") + } + if len(n.Country) == 0 && asn1dn.Country != "" { + n.Country = append(n.Country, asn1dn.Country) + } + if len(n.Organization) == 0 && asn1dn.Organization != "" { + n.Organization = append(n.Organization, asn1dn.Organization) + } + if len(n.OrganizationalUnit) == 0 && asn1dn.OrganizationalUnit != "" { + n.OrganizationalUnit = append(n.OrganizationalUnit, asn1dn.OrganizationalUnit) + } + if len(n.Locality) == 0 && asn1dn.Locality != "" { + n.Locality = append(n.Locality, asn1dn.Locality) + } + if len(n.Province) == 0 && asn1dn.Province != "" { + n.Province = append(n.Province, asn1dn.Province) + } + if len(n.StreetAddress) == 0 && asn1dn.StreetAddress != "" { + n.StreetAddress = append(n.StreetAddress, asn1dn.StreetAddress) + } + return nil +} + +// FromCSR generates a CertTemplate from a x509 certificate signing request. +func FromCSR(csr *x509.CertificateRequest, options ...func(*CertTemplate) error) (*CertTemplate, error) { + ct, err := NewCertTemplate(Hosts(csr.Subject.CommonName), + NotBeforeAfter(Now(), DefaultCertValidity), + ExtKeyUsage([]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}), + Subject(PkixName(csr.Subject))) + if err != nil { + return nil, err + } + for _, op := range options { + err = op(ct) + if err != nil { + return nil, err + } + } + return ct, nil +} + +// FromCert generates a CertTemplate from a x509 certificate. +func FromCert(cert *x509.Certificate, issuer pkix.Name) (*CertTemplate, error) { + return NewCertTemplate(Hosts(cert.Subject.CommonName), + NotBeforeAfter(Now(), DefaultCertValidity), + ExtKeyUsage([]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}), + Issuer(PkixName(issuer)), Subject(PkixName(cert.Subject))) +} diff --git a/crypto/certificates/x509/certTemplate_test.go b/crypto/certificates/x509/certTemplate_test.go new file mode 100644 index 00000000..74ea8fff --- /dev/null +++ b/crypto/certificates/x509/certTemplate_test.go @@ -0,0 +1,886 @@ +package x509 + +import ( + "crypto/x509" + "math/big" + "net" + "reflect" + "strings" + "testing" + "time" + + cmp "github.com/google/go-cmp/cmp" +) + +func Test_NotBeforeAfter(t *testing.T) { + var expected string + var ctv *CertTemplate + + // NotBefore zero defaults to Now + duration := time.Hour * 24 * 365 * 5 + ct, err := NewCertTemplate(NotBeforeAfter(time.Time{}, duration)) + if err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + ctv.NotBefore = time.Now().UTC() + ctv.NotAfter = ctv.NotBefore.Add(duration) + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + // Duration 0 uses default + ct, err = NewCertTemplate(NotBeforeAfter(time.Time{}, 0)) + if err != nil { + t.Errorf("Unexpected error: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + ctv.NotBefore = time.Now().UTC() + ctv.NotAfter = ctv.NotBefore.Add(defaultDuration) + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + // Duration < 0 returns error + _, err = NewCertTemplate(NotBeforeAfter(time.Time{}, -1)) + if err == nil { + t.Errorf("expected: , but got `nil`") + } else { + expected = "Duration must be greater than 0" + if strings.Compare(err.Error(), expected) != 0 { + t.Errorf("error mismatch -- expected: `%s ...`, but got: `%s`", + expected, err.Error()) + } + } + + // Duration negative returns error + _, err = NewCertTemplate(NotBeforeAfter(time.Time{}, -1)) + if err == nil { + t.Errorf("expected: , but got `nil`") + } else { + expected = "Duration must be greater than 0" + if strings.Compare(err.Error(), expected) != 0 { + t.Errorf("error mismatch -- expected: `%s ...`, but got: `%s`", + expected, err.Error()) + } + } + + // NotBefore and Duration set + now := time.Now().UTC() + duration = time.Hour * 24 * 365 * 5 + start := now.Add(time.Hour * 24 * 365 * 1) + ct, err = NewCertTemplate(NotBeforeAfter(start, duration)) + if err != nil { + t.Errorf("Unexpected error: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + ctv.NotBefore = start + ctv.NotAfter = start.Add(duration) + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + // Overwritten + now = time.Now().UTC() + duration = time.Hour * 24 * 365 * 5 + start = now.Add(time.Hour * 24 * 365 * 1) + ct, err = NewCertTemplate(NotBeforeAfter(start, duration), + NotBeforeAfter(time.Time{}, 0)) + if err != nil { + t.Errorf("Unexpected error: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } +} + +func Test_CRLSign(t *testing.T) { + var ctv *CertTemplate + + // false + ct, err := NewCertTemplate(CRLSign(false)) + if err != nil { + t.Errorf("NewCertTemplate: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + // true + ct, err = NewCertTemplate(CRLSign(true)) + if err != nil { + t.Errorf("NewCertTemplate: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + ctv.KeyUsage |= x509.KeyUsageCRLSign + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + // true -> false + ct, err = NewCertTemplate(CRLSign(true), CRLSign(false)) + if err != nil { + t.Errorf("NewCertTemplate: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + // true -> false -> true + ct, err = NewCertTemplate(CRLSign(true), CRLSign(false), CRLSign(true)) + if err != nil { + t.Errorf("NewCertTemplate: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + ctv.KeyUsage |= x509.KeyUsageCRLSign + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } +} + +func Test_Hosts(t *testing.T) { + var ctv *CertTemplate + var ct *CertTemplate + + // empty throws error + hosts := "" + _, err := NewCertTemplate(Hosts(hosts)) + if err != nil { + expected := "hosts cannot be empty" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch -- expected: `%s`, but got: `%s`", + expected, err.Error()) + } + } else { + t.Errorf("expected but not ") + } + + hosts = "127.0.0.1" + ct, err = NewCertTemplate(Hosts(hosts)) + if err != nil { + t.Errorf("NewCertTemplate: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + ctv.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")} + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + hosts = "127.0.0.1,smallstep.com,8.8.8.8,google.com" + ct, err = NewCertTemplate(Hosts(hosts)) + if err != nil { + t.Errorf("NewCertTemplate: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + ctv.DNSNames = []string{"smallstep.com", "google.com"} + ctv.IPAddresses = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("8.8.8.8")} + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + h1 := "127.0.0.1,smallstep.com,8.8.8.8,google.com" + h2 := "1.1.1.1,facebook.com" + ct, err = NewCertTemplate(Hosts(h1), Hosts(h2)) + if err != nil { + t.Errorf("NewCertTemplate: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + ctv.DNSNames = []string{"smallstep.com", "google.com", "facebook.com"} + ctv.IPAddresses = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("8.8.8.8"), + net.ParseIP("1.1.1.1")} + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } +} +func Test_SerialNumber(t *testing.T) { + var ctv *CertTemplate + var err error + var expected string + + // nil pointer returns error + _, err = NewCertTemplate(SerialNumber(nil)) + if err == nil { + t.Error("expected: , but got: nil") + } else { + expected = "SerialNumber cannot be nil" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch -- expected: `%s ...`, but got: `%s`", + expected, err.Error()) + } + } + + // empty string returns errordefaults to random + sn := "" + _, err = NewCertTemplate(SerialNumber(&sn)) + if err == nil { + t.Error("expected: , but got: nil") + } else { + expected = "SerialNumber cannot be nil" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch -- expected: `%s ...`, but got: `%s`", + expected, err.Error()) + } + } + + // NaN returns error + sn = "shake and bake" + _, err = NewCertTemplate(SerialNumber(&sn)) + if err == nil { + t.Error("expected: , but got: nil") + } else { + expected = "Failed to parse serial number: " + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch -- expected: `%s ...`, but got: `%s`", + expected, err.Error()) + } + } + + // valid value modifies SerialNumber + var success bool + sn = "51" + ct, err := NewCertTemplate(SerialNumber(&sn)) + if err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + ctv.SerialNumber, success = new(big.Int).SetString(sn, 10) + if !success { + t.Errorf("New big.Int failure") + } else { + if !reflect.DeepEqual(ctv.SerialNumber, ct.SerialNumber) { + t.Errorf("SerialNumber mismatch -- expected: `%s`, but got: `%s`", + ctv.SerialNumber.String(), ct.SerialNumber.String()) + } + } + } + } +} + +func Test_Subject(t *testing.T) { + var ctv *CertTemplate + var pn PkixName + + // Empty + ct, err := NewCertTemplate(Subject(PkixName{})) + if err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + // with values + pn = PkixName{ + Country: []string{"usa"}, + Organization: []string{"smallstep"}, + Locality: []string{"san francisco"}, + CommonName: "internal.smallstep.com", + } + ct, err = NewCertTemplate(Subject(pn)) + if err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + ctv.Subject.Country = []string{"usa"} + ctv.Subject.Organization = []string{"smallstep"} + ctv.Subject.Locality = []string{"san francisco"} + ctv.Subject.CommonName = "internal.smallstep.com" + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } +} + +func Test_Issuer(t *testing.T) { + var ctv *CertTemplate + var pn PkixName + + // Empty + ct, err := NewCertTemplate(Issuer(PkixName{})) + if err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + // with values + pn = PkixName{ + Country: []string{"usa"}, + Organization: []string{"smallstep"}, + Locality: []string{"san francisco"}, + CommonName: "internal.smallstep.com", + } + ct, err = NewCertTemplate(Issuer(pn)) + if err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + ctv.Issuer.Country = []string{"usa"} + ctv.Issuer.Organization = []string{"smallstep"} + ctv.Issuer.Locality = []string{"san francisco"} + ctv.Issuer.CommonName = "internal.smallstep.com" + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } +} + +func Test_BasicConstraints(t *testing.T) { + var ctv *CertTemplate + var ct *CertTemplate + var err error + var expected string + + // unset + ct, err = NewCertTemplate() + if err != nil { + t.Errorf("NewCertTemplate: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + // if BasicConstraintsValid==false and IsCA=true then error + ct, err = NewCertTemplate(BasicConstraints(false, true, 0)) + if err != nil { + expected = "isCA must be `false` if `BasicConstraintsValid==false`" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch -- expected: `%s`, but got: `%s`", + expected, err) + } + } else { + t.Errorf("expected: `error`, but got: `nil`") + } + + // BCV==false and IsCA false + ct, err = NewCertTemplate(BasicConstraints(false, false, 0)) + if err != nil { + t.Errorf("NewCertTemplate: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + // BCV==false and IsCA false + ct, err = NewCertTemplate(BasicConstraints(true, true, 0)) + if err != nil { + t.Errorf("NewCertTemplate: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + ctv.IsCA = true + ctv.KeyUsage |= x509.KeyUsageCertSign + ctv.BasicConstraintsValid = true + ctv.MaxPathLen = 0 + ctv.MaxPathLenZero = true + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + // overwrite + ct, err = NewCertTemplate(BasicConstraints(true, true, 0), BasicConstraints(true, false, 0)) + if err != nil { + t.Errorf("NewCertTemplate: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + ctv.IsCA = false + ctv.BasicConstraintsValid = true + ctv.MaxPathLen = 0 + ctv.MaxPathLenZero = true + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + // BCV==true and maxPathLen < 0 returns error + ct, err = NewCertTemplate(BasicConstraints(true, false, -4)) + if err == nil { + t.Errorf("expected: `error`, but got: `nil`") + } else { + expected = "MaxPathLen must be >= 0" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch -- expected: `%s`, but got: `%s`", + expected, err) + } + } + + // explicitly set MaxPathlen to 0 + mpl := 0 + ct, err = NewCertTemplate(BasicConstraints(true, false, mpl)) + if err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if ct.MaxPathLen != mpl { + t.Errorf("MaxPathLen -- expected: `%d`, but got: `%d`", + mpl, ct.MaxPathLen) + } + if ct.MaxPathLenZero != true { + t.Errorf("MaxPathLenZero mismatch -- expected: `%t`, but got: `%t`", + true, ct.MaxPathLenZero) + } + } + + // MaxPathLen non-zero + mpl = 3 + ct, err = NewCertTemplate(BasicConstraints(true, false, mpl)) + if err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + ctv.BasicConstraintsValid = true + ctv.MaxPathLen = mpl + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + // overwrite MaxPathLen + ct, err = NewCertTemplate(BasicConstraints(true, false, 3), BasicConstraints(true, false, 0)) + if err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + ctv.BasicConstraintsValid = true + ctv.MaxPathLen = 0 + ctv.MaxPathLenZero = true + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + // BCV==false and maxPathLen != 0 returns error + ct, err = NewCertTemplate(BasicConstraints(false, false, 5)) + if err != nil { + expected = "maxPathLen should be set to 0 if `BasicConstraintsValid==false`" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch -- expected: `%s`, but got: `%s`", + expected, err) + } + } else { + t.Errorf("expected: `error`, but got: `nil`") + } + + // overwrite MaxPathLen + ct, err = NewCertTemplate(BasicConstraints(false, false, 0), + BasicConstraints(true, true, 3), BasicConstraints(false, false, 0)) + if err != nil { + t.Errorf("NewCertTemplate: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } +} + +func Test_NewCertTemplate(t *testing.T) { + var ctv *CertTemplate + + // empty + ct, err := NewCertTemplate() + if err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if ct.SerialNumber == nil { + t.Errorf("SerialNumber cannot be nil. A random one should have been created") + } + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } + + // error + sn := "shake and bake" + hosts := "google.com,127.0.0.1" + ct, err = NewCertTemplate(Hosts(hosts), SerialNumber(&sn), BasicConstraints(true, true, 0)) + if err == nil { + t.Error("expected: , but got: nil") + } else { + expected := "Failed to parse serial number: " + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("bad error message -- expected: `%s ...`, but got: `%s`", + expected, err.Error()) + } + } + + // with values + sn = "51" + hosts = "google.com,127.0.0.1,facebook.com,1.1.1.1" + now := time.Now().UTC() + duration := time.Hour * 24 * 365 * 5 + start := now.Add(time.Hour * 24 * 365 * 1) + pn := PkixName{ + Country: []string{"usa"}, + Organization: []string{"smallstep"}, + Locality: []string{"san francisco"}, + CommonName: "internal.smallstep.com", + } + ct, err = NewCertTemplate(Hosts(hosts), BasicConstraints(true, true, 0), + Subject(pn), Issuer(pn), NotBeforeAfter(start, duration)) + if err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + if ctv, err = NewCertTemplate(); err != nil { + t.Errorf("NewCertTemplate error: %s", err) + } else { + ctv.NotBefore = start + ctv.NotAfter = start.Add(duration) + ctv.Subject.Country = []string{"usa"} + ctv.Subject.Organization = []string{"smallstep"} + ctv.Subject.Locality = []string{"san francisco"} + ctv.Subject.CommonName = "internal.smallstep.com" + ctv.Issuer.Country = []string{"usa"} + ctv.Issuer.Organization = []string{"smallstep"} + ctv.Issuer.Locality = []string{"san francisco"} + ctv.Issuer.CommonName = "internal.smallstep.com" + ctv.DNSNames = []string{"google.com", "facebook.com"} + ctv.IPAddresses = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("1.1.1.1")} + ctv.IsCA = true + ctv.KeyUsage |= x509.KeyUsageCertSign + ctv.BasicConstraintsValid = true + ctv.MaxPathLen = 0 + ctv.MaxPathLenZero = true + if err = ctv.Compare(*ct); err != nil { + t.Errorf("%s", err) + } + } + } +} + +func Test_Locality(t *testing.T) { + var err error + var expected string + var pn, pnv *PkixName + + // empty throws error + locality := "" + _, err = NewPkixName(Locality(locality)) + if err != nil { + expected = "localities cannot be empty" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch -- expected: `%s`, but got: `%s`", + expected, err.Error()) + } + } else { + t.Errorf("expected: `error`, but got: `nil`") + } + + // normal single + locality = "boston" + pn, err = NewPkixName(Locality(locality)) + if err != nil { + t.Errorf("NewPkixName: %s", err) + } else { + pnv = &PkixName{Locality: strings.Split(locality, ",")} + if !cmp.Equal(pnv, pn) { + t.Errorf("data mismatch") + } + } + + // normal list + locality = "boston,philadelphia,manhattan" + pn, err = NewPkixName(Locality(locality)) + if err != nil { + t.Errorf("NewPkixName: %s", err) + } else { + pnv = &PkixName{Locality: strings.Split(locality, ",")} + if !cmp.Equal(pnv, pn) { + t.Errorf("data mismatch") + } + } + + // overwrite + l1 := "brazil,italy,sudan" + l2 := "ukraine,russia,china" + pn, err = NewPkixName(Locality(l1), Locality(l2)) + if err != nil { + t.Errorf("NewPkixName: %s", err) + } else { + pnv = &PkixName{Locality: append(strings.Split(l1, ","), strings.Split(l2, ",")...)} + if !cmp.Equal(pnv, pn) { + t.Errorf("data mismatch") + } + } +} + +func Test_Country(t *testing.T) { + var err error + var expected string + var pn, pnv *PkixName + + // empty throws error + pn, err = NewPkixName(Country("")) + if err != nil { + expected = "countries cannot be empty" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch -- expected: `%s`, but got: `%s`", + expected, err.Error()) + } + } else { + pnv = &PkixName{} + if !cmp.Equal(pnv, pn) { + t.Errorf("data mismatch") + } + } + + // normal single + country := "brazil" + pn, err = NewPkixName(Country(country)) + if err != nil { + t.Errorf("NewPkixName: %s", err) + } else { + pnv = &PkixName{Country: strings.Split(country, ",")} + if !cmp.Equal(pnv, pn) { + t.Errorf("data mismatch") + } + } + + // normal list + country = "brazil,italy,sudan" + pn, err = NewPkixName(Country(country)) + if err != nil { + t.Errorf("NewPkixName: %s", err) + } else { + pnv = &PkixName{Country: strings.Split(country, ",")} + if !cmp.Equal(pnv, pn) { + t.Errorf("data mismatch") + } + } + + // overwrite + c1 := "brazil,italy,sudan" + c2 := "ukraine,russia,china" + pn, err = NewPkixName(Country(c1), Country(c2)) + if err != nil { + t.Errorf("NewPkixName: %s", err) + } else { + pnv = &PkixName{Country: append(strings.Split(c1, ","), strings.Split(c2, ",")...)} + if !cmp.Equal(pnv, pn) { + t.Errorf("data mismatch") + } + } +} + +func Test_Organization(t *testing.T) { + var err error + var expected string + var orgs string + var pn, pnv *PkixName + + // empty throws error + orgs = "" + pn, err = NewPkixName(Organization(orgs)) + if err != nil { + expected = "orgs cannot be empty" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch -- expected: `%s`, but got: `%s`", + expected, err.Error()) + } + } else { + pnv = &PkixName{} + if !cmp.Equal(pnv, pn) { + t.Errorf("data mismatch") + } + } + + // normal single + orgs = "smallstep" + pn, err = NewPkixName(Organization(orgs)) + if err != nil { + t.Errorf("NewPkixName: %s", err) + } else { + pnv = &PkixName{Organization: strings.Split(orgs, ",")} + if !cmp.Equal(pnv, pn) { + t.Errorf("data mismatch") + } + } + + // normal list + orgs = "smallstep,betable,oracle" + pn, err = NewPkixName(Organization(orgs)) + if err != nil { + t.Errorf("NewPkixName: %s", err) + } else { + pnv = &PkixName{Organization: strings.Split(orgs, ",")} + if !cmp.Equal(pnv, pn) { + t.Errorf("data mismatch") + } + } + + // overwrite + org1 := "smallstep,betable,oracle" + org2 := "google,facebook,apple" + pn, err = NewPkixName(Organization(org1), Organization(org2)) + if err != nil { + t.Errorf("NewPkixName: %s", err) + } else { + pnv = &PkixName{Organization: append(strings.Split(org1, ","), strings.Split(org2, ",")...)} + if !cmp.Equal(pnv, pn) { + t.Errorf("data mismatch") + } + } +} + +func Test_CommonName(t *testing.T) { + var err error + var expected string + var pn, pnv *PkixName + + // empty throws error + cn := "" + pn, err = NewPkixName(CommonName(cn)) + if err != nil { + expected = "common cannot be empty" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch -- expected: `%s`, but got: `%s`", + expected, err.Error()) + } + } else { + pnv = &PkixName{} + if !cmp.Equal(pnv, pn) { + t.Errorf("data mismatch") + } + } + + // normal + cn = "internal.smallstep.com" + pn, err = NewPkixName(CommonName(cn)) + if err != nil { + t.Errorf("NewPkixName: %s", err) + } else { + pnv = &PkixName{CommonName: cn} + if !cmp.Equal(pnv, pn) { + t.Errorf("data mismatch") + } + } + + // overwrite + cn = "internal.smallstep.com" + cn2 := "smallstep.com" + pn, err = NewPkixName(CommonName(cn), CommonName(cn2)) + if err != nil { + t.Errorf("NewPkixName: %s", err) + } else { + pnv = &PkixName{CommonName: cn2} + if !cmp.Equal(pnv, pn) { + t.Errorf("data mismatch") + } + } +} + +func Test_NewPkixName(t *testing.T) { + cn := "internal.smallstep.com" + country := "brazil,italy,sudan" + org := "smallstep,betable,oracle" + locality := "boston,philadelphia,manhattan" + pn, err := NewPkixName(CommonName(cn), Organization(org), Country(country), + Locality(locality)) + if err != nil { + t.Errorf("NewPkixName: %s", err) + } else { + pnv := &PkixName{ + Organization: strings.Split(org, ","), + Locality: strings.Split(locality, ","), + Country: strings.Split(country, ","), + CommonName: cn, + } + if !cmp.Equal(pnv, pn) { + t.Errorf("data mismatch") + } + } +} diff --git a/crypto/certificates/x509/clean_test.go b/crypto/certificates/x509/clean_test.go new file mode 100644 index 00000000..14caa770 --- /dev/null +++ b/crypto/certificates/x509/clean_test.go @@ -0,0 +1,26 @@ +package x509 + +import ( + "io/ioutil" + "log" + "os" + "testing" +) + +func TestMain(m *testing.M) { + // discard log output when testing + log.SetOutput(ioutil.Discard) + + result := m.Run() + + clean := func(files []string) { + for _, f := range files { + if _, err := os.Stat(f); !os.IsNotExist(err) { + os.Remove(f) + } + } + } + clean([]string{"./test.crt"}) + + os.Exit(result) +} diff --git a/crypto/certificates/x509/crt.go b/crypto/certificates/x509/crt.go new file mode 100644 index 00000000..ef73ba75 --- /dev/null +++ b/crypto/certificates/x509/crt.go @@ -0,0 +1,99 @@ +package x509 + +import ( + "crypto/x509" + "encoding/pem" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" +) + +// WriteCertificate encodes a x509 Certificate to a file on disk in PEM format. +func WriteCertificate(crt []byte, out string) error { + if crt == nil { + return errors.Errorf("crt cannot be nil") + } + certOut, err := os.OpenFile(out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, + os.FileMode(0644)) + if err != nil { + return errors.Wrapf(err, + "failed to open '%s' for writing", out) + } + err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: crt}) + if err != nil { + return errors.Wrapf(err, + "pem encode '%s' failed", out) + } + certOut.Close() + return nil +} + +// LoadCertificate load a certificate. +func LoadCertificate(crtPath string) (*x509.Certificate, *pem.Block, error) { + publicBytes, err := ioutil.ReadFile(crtPath) + if err != nil { + return nil, nil, errors.Wrapf(err, "error opening certificate file %s", crtPath) + } + publicPEM, _ := pem.Decode(publicBytes) + if publicPEM == nil { + return nil, nil, errors.Errorf("error decoding certificate file %s", crtPath) + } + crt, err := x509.ParseCertificate(publicPEM.Bytes) + if err != nil { + return nil, nil, errors.Wrapf(err, "error parsing x509 certificate file %s", crtPath) + } + + return crt, publicPEM, nil +} + +// ReadCertPool loads a certificate pool from disk. +func ReadCertPool(path string) (*x509.CertPool, error) { + info, err := os.Stat(path) + if err != nil { + return nil, errors.WithStack(err) + } + + var ( + files []string + pool = x509.NewCertPool() + ) + if info.IsDir() { + finfos, err := ioutil.ReadDir(path) + if err != nil { + return nil, errors.WithStack(err) + } + for _, finfo := range finfos { + files = append(files, filepath.Join(path, finfo.Name())) + } + } else { + files = strings.Split(path, ",") + } + + var pems []byte + for _, f := range files { + bytes, err := ioutil.ReadFile(f) + if err != nil { + return nil, errors.WithStack(err) + } + for len(bytes) > 0 { + var block *pem.Block + block, bytes = pem.Decode(bytes) + if block == nil { + // TODO: at a higher log level we should log the file we could not find. + break + } + // Ignore PEM blocks that are not CERTIFICATEs. + if block.Type != "CERTIFICATE" { + continue + } + pems = append(pems, pem.EncodeToMemory(block)...) + } + } + if ok := pool.AppendCertsFromPEM(pems); !ok { + return nil, errors.Errorf("error loading Root certificates") + } + return pool, nil +} diff --git a/crypto/certificates/x509/crt_test.go b/crypto/certificates/x509/crt_test.go new file mode 100644 index 00000000..fdc5e69e --- /dev/null +++ b/crypto/certificates/x509/crt_test.go @@ -0,0 +1,157 @@ +package x509 + +import ( + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "io/ioutil" + "net" + "strings" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/assert" +) + +func Test_WriteCertificate(t *testing.T) { + certPath := "./test.crt" + + tests := map[string]struct { + crt func() ([]byte, error) + crtOut string + err error + }{ + "crt cannot be nil": { + crt: func() ([]byte, error) { + return nil, nil + }, + err: errors.New("crt cannot be nil"), + }, + "propagate open crt out file error": { + crt: func() ([]byte, error) { + return []byte{}, nil + }, + crtOut: "./fakeDir/test.crt", + err: errors.New("failed to open './fakeDir/test.crt' for writing: open ./fakeDir/test.crt: no such file or directory"), + }, + "success": { + crt: func() ([]byte, error) { + hosts := "google.com,127.0.0.1,facebook.com,1.1.1.1" + sub := pkix.Name{ + Country: []string{"usa"}, + Organization: []string{"smallstep"}, + Locality: []string{"san francisco"}, + CommonName: "internal.smallstep.com", + } + profile, err := NewRootProfile("overwrite", WithSubject(sub), + WithIssuer(sub), WithHosts(hosts)) + if err != nil { + return nil, err + } + return profile.CreateCertificate() + }, + crtOut: certPath, + }, + } + + for name, test := range tests { + t.Logf("Running test case: %s", name) + + crtBytes, err := test.crt() + assert.FatalError(t, err) + + err = WriteCertificate(crtBytes, test.crtOut) + if err != nil { + if assert.NotNil(t, test.err) { + assert.HasPrefix(t, err.Error(), test.err.Error()) + } + } else { + ctv, err := NewCertTemplate() + assert.FatalError(t, err) + ctv.Subject.Country = []string{"usa"} + ctv.Subject.Organization = []string{"smallstep"} + ctv.Subject.Locality = []string{"san francisco"} + ctv.Subject.CommonName = "internal.smallstep.com" + ctv.Issuer.Country = []string{"usa"} + ctv.Issuer.Organization = []string{"smallstep"} + ctv.Issuer.Locality = []string{"san francisco"} + ctv.Issuer.CommonName = "internal.smallstep.com" + ctv.DNSNames = []string{"google.com", "facebook.com"} + ctv.IPAddresses = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("1.1.1.1")} + ctv.IsCA = true + ctv.KeyUsage |= x509.KeyUsageKeyEncipherment | + x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | + x509.KeyUsageCRLSign + ctv.BasicConstraintsValid = true + ctv.MaxPathLenZero = false + ctv.MaxPathLen = 1 + ctv.SignatureAlgorithm = x509.ECDSAWithSHA256 + ctv.PublicKeyAlgorithm = x509.ECDSA + ctv.Version = 3 + + crt, err := x509.ParseCertificate(crtBytes) + assert.FatalError(t, err) + pubBytes, err := x509.MarshalPKIXPublicKey(crt.PublicKey) + assert.FatalError(t, err) + _hash := sha1.Sum(pubBytes) + hash := _hash[:] + ctv.SubjectKeyId = hash[:] // takes slice over the whole array + + // Verify that cert written to file is correct + certFileBytes, err := ioutil.ReadFile(certPath) + assert.FatalError(t, err) + pemCert, _ := pem.Decode(certFileBytes) + fileCert, err := x509.ParseCertificate(pemCert.Bytes) + assert.FatalError(t, err) + + // Check `NotBefore` and `NotAfter`. + now := time.Now().UTC() + assert.True(t, fileCert.NotBefore.Before(now)) + assert.True(t, fileCert.NotBefore.After(now.Add(-time.Minute))) + expiry := now.Add(time.Hour * 24 * 365 * 10) + assert.True(t, fileCert.NotAfter.Before(expiry)) + assert.True(t, fileCert.NotAfter.After(expiry.Add(-time.Minute))) + // Now we set these to correct values since we've already checked them. + ctv.NotBefore = fileCert.NotBefore + ctv.NotAfter = fileCert.NotAfter + + assert.NoError(t, ctv.Compare(CertTemplate(*fileCert))) + } + } +} + +func Test_LoadCertificate(t *testing.T) { + var ( + testBadCert = "./test_files/badca.crt" + testBadPEMCert = "./test_files/badpem.crt" + testCert = "./test_files/ca.crt" + ) + + e1 := fmt.Sprintf("error decoding certificate file %s", testBadPEMCert) + e2 := fmt.Sprintf("error parsing x509 certificate file %s", testBadCert) + + tests := []struct { + crtPath string + expectedError string + }{ + {"", "error opening certificate file "}, + {"", "error opening certificate file "}, + {testBadPEMCert, e1}, + {testBadCert, e2}, + } + + for i, tc := range tests { + _, _, err := LoadCertificate(tc.crtPath) + if assert.Error(t, err) { + assert.True(t, strings.HasPrefix(err.Error(), tc.expectedError), err.Error(), i) + } + } + + crt, pemBlock, err := LoadCertificate(testCert) + assert.FatalError(t, err) + assert.Equals(t, crt.Subject.CommonName, "internal.smallstep.com") + assert.NotNil(t, pemBlock) +} diff --git a/crypto/certificates/x509/csr.go b/crypto/certificates/x509/csr.go new file mode 100644 index 00000000..1d4e8317 --- /dev/null +++ b/crypto/certificates/x509/csr.go @@ -0,0 +1,16 @@ +package x509 + +import ( + "crypto/x509" + "encoding/pem" + "errors" +) + +// LoadCSRFromBytes loads a CSR given the ASN.1 DER format. +func LoadCSRFromBytes(der []byte) (*x509.CertificateRequest, error) { + block, _ := pem.Decode(der) + if block == nil { + return nil, errors.New("failed to decode PEM block containing CSR") + } + return x509.ParseCertificateRequest(block.Bytes) +} diff --git a/crypto/certificates/x509/csr_test.go b/crypto/certificates/x509/csr_test.go new file mode 100644 index 00000000..a07524be --- /dev/null +++ b/crypto/certificates/x509/csr_test.go @@ -0,0 +1,78 @@ +package x509 + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "testing" + + "github.com/pkg/errors" + "github.com/smallstep/assert" +) + +func Test_CSR_LoadCSRFromBytes(t *testing.T) { + tests := map[string]struct { + der func() ([]byte, error) + err error + }{ + "propagate pem decode error": { + der: func() ([]byte, error) { + return nil, nil + }, + err: errors.New("failed to decode PEM block containing CSR"), + }, + "propagate parse error": { + der: func() ([]byte, error) { + return []byte("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyEU5ZhIhFn7v4bpMKlkz\ndmLCj9KfmqFWig29c6OzYoMUnbdodOmZ6RId/Gw5fnluH12eFxsItlXKDT4RPSm7\nm4D1sYgFmk88oo6z4XDuItDncoIg89jGK38OZ8A0gwEoy5JqukONGmAldzgzQyiq\nuzSNMeT1WO9zXCwOljcUio697M1kP/YN1Lp7n7YILVwdV8wQ2vyNKQK1M/5OZOFl\nlOqww4wsqLTDK0rfxp6LAVtczp1XdxbsnpdixrK38O+dHWe4IS5HhKLmmmdTfpFQ\nD3PIAXs/Naap/0+t0lsOplNPiF4BYyNBIqyyfm1o5ZQpGfmITKvDFZMBkQ2i2cou\nnQIDAQAB\n-----END PUBLIC KEY-----"), nil + }, + err: errors.New("asn1: structure error: tags don't match"), + }, + "success": { + der: func() ([]byte, error) { + keypair, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, err + } + template := &x509.CertificateRequest{ + Subject: pkix.Name{ + Country: []string{"Foo"}, + Organization: []string{"Smallstep"}, + CommonName: "Bar", + }, + SignatureAlgorithm: x509.SHA256WithRSA, + } + + bytes, err := x509.CreateCertificateRequest(rand.Reader, template, keypair) + if err != nil { + return nil, err + } + + return pem.EncodeToMemory(&pem.Block{ + Type: "CSR", + Headers: map[string]string{}, + Bytes: bytes, + }), nil + }, + }, + } + + for name, test := range tests { + t.Logf("Running test case: %s", name) + + bytes, err := test.der() + assert.FatalError(t, err) + csr, err := LoadCSRFromBytes(bytes) + + if err != nil { + if assert.NotNil(t, test.err) { + assert.HasPrefix(t, err.Error(), test.err.Error()) + } + } else { + assert.Equals(t, csr.Subject.Country, []string{"Foo"}) + assert.Equals(t, csr.Subject.Organization, []string{"Smallstep"}) + assert.Equals(t, csr.Subject.CommonName, "Bar") + } + } +} diff --git a/crypto/certificates/x509/identity.go b/crypto/certificates/x509/identity.go new file mode 100644 index 00000000..e6330ed2 --- /dev/null +++ b/crypto/certificates/x509/identity.go @@ -0,0 +1,52 @@ +package x509 + +import ( + "crypto/x509" + "encoding/pem" + "io/ioutil" + + "github.com/pkg/errors" + "github.com/smallstep/cli/crypto/keys" +) + +// Identity contains a public/private x509 certificate/key pair. +type Identity struct { + Crt *x509.Certificate + CrtPem *pem.Block + Key interface{} +} + +// NewIdentity returns a new Identity. +func NewIdentity(c *x509.Certificate, b *pem.Block, k interface{}) *Identity { + return &Identity{ + Crt: c, + CrtPem: b, + Key: k, + } +} + +// LoadIdentityFromDisk load a public certificate and private key from disk. +func LoadIdentityFromDisk(crtPath, keyPath string, getPass func() (string, error)) (*Identity, error) { + var ( + err error + caCert *x509.Certificate + caPrivateKey interface{} + publicPem *pem.Block + ) + + // load crt + if caCert, publicPem, err = LoadCertificate(crtPath); err != nil { + return nil, errors.WithStack(err) + } + + // load private key + keyBytes, err := ioutil.ReadFile(keyPath) + if err != nil { + return nil, errors.WithStack(err) + } + if caPrivateKey, err = keys.LoadPrivateKey(keyBytes, getPass); err != nil { + return nil, errors.WithStack(err) + } + + return NewIdentity(caCert, publicPem, caPrivateKey), nil +} diff --git a/crypto/certificates/x509/identity_test.go b/crypto/certificates/x509/identity_test.go new file mode 100644 index 00000000..7850e88b --- /dev/null +++ b/crypto/certificates/x509/identity_test.go @@ -0,0 +1,61 @@ +package x509 + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/smallstep/assert" +) + +func Test_LoadIdentityFromDisk(t *testing.T) { + var ( + testBadCert = "./test_files/badca.crt" + testCert = "./test_files/ca.crt" + testNoPasscodeBadKey = "./test_files/noPasscodeBadCa.key" + noPasscodeKey = "./test_files/noPasscodeCa.key" + ) + + tests := map[string]struct { + crtPath string + keyPath string + pass string + err error + }{ + "error parsing x509 certificate": { + crtPath: testBadCert, + keyPath: "", + pass: "", + err: errors.Errorf("error parsing x509 certificate file %s", + testBadCert), + }, + "error parsing rsa key": { + crtPath: testCert, + keyPath: testNoPasscodeBadKey, + pass: "", + err: errors.Errorf("error parsing RSA key"), + }, + "success": { + crtPath: testCert, + keyPath: noPasscodeKey, + pass: "", + }, + } + + for name, test := range tests { + t.Logf("Running test case: %s", name) + + kp, err := LoadIdentityFromDisk(test.crtPath, test.keyPath, func() (string, error) { + return test.pass, nil + }) + if err != nil { + if assert.NotNil(t, test.err) { + assert.HasPrefix(t, err.Error(), test.err.Error()) + } + } else { + assert.FatalError(t, err) + assert.NotNil(t, kp.Crt) + assert.NotNil(t, kp.CrtPem) + assert.NotNil(t, kp.Key) + } + } +} diff --git a/crypto/certificates/x509/intermediateProfile.go b/crypto/certificates/x509/intermediateProfile.go new file mode 100644 index 00000000..0a2897b9 --- /dev/null +++ b/crypto/certificates/x509/intermediateProfile.go @@ -0,0 +1,63 @@ +package x509 + +import ( + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "time" + + "github.com/pkg/errors" +) + +// Intermediate implements the Profile for a intermediate certificate. +type Intermediate struct { + base +} + +// NewIntermediateProfile returns a new intermediate x509 Certificate profile. +func NewIntermediateProfile(name string, iss *x509.Certificate, issPriv interface{}, withOps ...WithOption) (*Intermediate, error) { + var ( + err error + notBefore = time.Now() + ) + + sub := &x509.Certificate{ + IsCA: true, + NotBefore: notBefore, + // 10 year intermediate certificate validity. + NotAfter: notBefore.Add(time.Hour * 24 * 365 * 10), + KeyUsage: x509.KeyUsageKeyEncipherment | + x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + x509.ExtKeyUsageClientAuth, + }, + BasicConstraintsValid: true, + MaxPathLen: 0, + MaxPathLenZero: true, + Issuer: pkix.Name{CommonName: name}, + Subject: pkix.Name{CommonName: name}, + } + + b, err := newBase(sub, iss, withOps...) + if err != nil { + return nil, err + } + + if sub.SerialNumber == nil { + // TODO figure out how to test rand w/out threading as another arg + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + sub.SerialNumber, err = rand.Int(rand.Reader, serialNumberLimit) + // TODO error condition untested -- hard to test w/o mocking rand + if err != nil { + return nil, errors.Wrapf(err, "Failed to generate serial number for "+ + "certificate with common name '%s'", name) + } + } + + i := &Intermediate{} + fromBase(i, *b) + i.SetIssuerPrivateKey(issPriv) + return i, nil +} diff --git a/crypto/certificates/x509/leafProfile.go b/crypto/certificates/x509/leafProfile.go new file mode 100644 index 00000000..94339690 --- /dev/null +++ b/crypto/certificates/x509/leafProfile.go @@ -0,0 +1,106 @@ +package x509 + +import ( + "crypto/x509" + "crypto/x509/pkix" + "time" + + "github.com/pkg/errors" +) + +// Leaf implements the Profile for a leaf certificate. +type Leaf struct { + base +} + +// NewLeafProfileWithTemplate returns a new leaf x509 Certificate Profile with +// Subject Certificate set to the value of the template argument. +// A public/private keypair **WILL NOT** be generated for this profile because +// the public key will be populated from the Subject Certificate parameter. +func NewLeafProfileWithTemplate(sub *x509.Certificate, iss *x509.Certificate, issPriv interface{}, withOps ...WithOption) (*Leaf, error) { + withOps = append(withOps, WithPublicKey(sub.PublicKey)) + b, err := newBase(sub, iss, withOps...) + if err != nil { + return nil, errors.WithStack(err) + } + + l := &Leaf{} + fromBase(l, *b) + l.SetIssuerPrivateKey(issPriv) + return l, nil +} + +// NewLeafProfile returns a new leaf x509 Certificate profile. +// A new public/private key pair will be generated for the Profile if +// not set in the `withOps` profile modifiers. +func NewLeafProfile(cn string, iss *x509.Certificate, issPriv interface{}, withOps ...WithOption) (*Leaf, error) { + sub, err := defaultLeafTemplate(pkix.Name{CommonName: cn}, iss.Subject) + if err != nil { + return nil, errors.WithStack(err) + } + + b, err := newBase(sub, iss, withOps...) + if err != nil { + return nil, errors.WithStack(err) + } + + l := &Leaf{} + fromBase(l, *b) + l.SetIssuerPrivateKey(issPriv) + return l, nil +} + +// NewLeafProfileWithCSR returns a new leaf x509 Certificate Profile with +// Subject Certificate fields populated directly from the CSR. +// A public/private keypair **WILL NOT** be generated for this profile because +// the public key will be populated from the CSR. +func NewLeafProfileWithCSR(csr *x509.CertificateRequest, iss *x509.Certificate, issPriv interface{}, withOps ...WithOption) (*Leaf, error) { + if csr.PublicKey == nil { + return nil, errors.Errorf("CSR must have PublicKey") + } + withOps = append(withOps, WithPublicKey(csr.PublicKey)) + + sub, err := defaultLeafTemplate(csr.Subject, iss.Subject) + if err != nil { + return nil, errors.WithStack(err) + } + sub.Extensions = csr.Extensions + sub.ExtraExtensions = csr.ExtraExtensions + sub.DNSNames = csr.DNSNames + sub.EmailAddresses = csr.EmailAddresses + sub.IPAddresses = csr.IPAddresses + sub.URIs = csr.URIs + + b, err := newBase(sub, iss, withOps...) + if err != nil { + return nil, errors.WithStack(err) + } + + l := &Leaf{} + fromBase(l, *b) + l.SetIssuerPrivateKey(issPriv) + return l, nil +} + +func defaultLeafTemplate(sub pkix.Name, iss pkix.Name) (*x509.Certificate, error) { + notBefore := time.Now() + + ct := &x509.Certificate{ + IsCA: false, + NotBefore: notBefore, + // 1 Day Leaf Certificate validity. + NotAfter: notBefore.Add(time.Hour * 24), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + x509.ExtKeyUsageClientAuth, + }, + BasicConstraintsValid: false, + MaxPathLen: 0, + MaxPathLenZero: false, + Issuer: iss, + Subject: sub, + } + + return ct, nil +} diff --git a/crypto/certificates/x509/profile.go b/crypto/certificates/x509/profile.go new file mode 100644 index 00000000..833db62c --- /dev/null +++ b/crypto/certificates/x509/profile.go @@ -0,0 +1,259 @@ +package x509 + +import ( + "crypto/rand" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "net" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/cli/crypto/keys" +) + +// Profile is an interface that certificate profiles (e.g. leaf, +// intermediate, root) must implement. +type Profile interface { + Issuer() *x509.Certificate + Subject() *x509.Certificate + SubjectPrivateKey() interface{} + SubjectPublicKey() interface{} + SetIssuer(*x509.Certificate) + SetSubject(*x509.Certificate) + SetSubjectPrivateKey(interface{}) + SetSubjectPublicKey(interface{}) + SetIssuerPrivateKey(interface{}) + CreateCertificate() ([]byte, error) + GenerateKeyPair(string, string, int) error +} + +type base struct { + iss *x509.Certificate + sub *x509.Certificate + subPub interface{} + subPriv interface{} + issPriv interface{} +} + +// WithOption is a modifier function on base. +type WithOption func(Profile) error + +// GenerateKeyPair returns a Profile modifier that generates a public/private +// key pair for a profile. +func GenerateKeyPair(kty, crv string, size int) WithOption { + return func(p Profile) error { + return p.GenerateKeyPair(kty, crv, size) + } +} + +// WithPublicKey returns a Profile modifier that sets the public key for a profile. +func WithPublicKey(pub interface{}) WithOption { + return func(p Profile) error { + p.SetSubjectPublicKey(pub) + return nil + } +} + +// WithSubject returns a Profile modifier that sets the Subject for a x509 +// Certificate. +func WithSubject(sub pkix.Name) WithOption { + return func(p Profile) error { + crt := p.Subject() + crt.Subject = sub + return nil + } +} + +// WithIssuer returns a Profile modifier that sets the Subject for a x509 +// Certificate. +func WithIssuer(iss pkix.Name) WithOption { + return func(p Profile) error { + crt := p.Subject() + crt.Issuer = iss + return nil + } +} + +// WithNotBeforeAfter returns a Profile modifier that sets the `NotBefore` and +// `NotAfter` attributes of the subject x509 Certificate. +func WithNotBeforeAfter(nb, na time.Time) WithOption { + return func(p Profile) error { + crt := p.Subject() + crt.NotBefore = nb + crt.NotAfter = na + return nil + } +} + +// WithHosts returns a Profile modifier which sets the DNS Names and IP Addresses +// that will be bound to the subject Certificate. +// +// `hosts` should be a comma separated string of DNS Names and IP Addresses. +// e.g. `127.0.0.1,internal.smallstep.com,blog.smallstep.com,1.1.1.1`. +func WithHosts(hosts string) WithOption { + return func(p Profile) error { + hostsL := strings.Split(hosts, ",") + crt := p.Subject() + for _, h := range hostsL { + if h == "" { + continue + } else if ip := net.ParseIP(h); ip != nil { + crt.IPAddresses = append(crt.IPAddresses, ip) + } else { + crt.DNSNames = append(crt.DNSNames, h) + } + } + + return nil + } +} + +// newBase generates a new base profile. +// +// If the public/private key pair of the subject identity are not set by +// the optional modifiers then a pair will be generated using sane defaults. +func newBase(sub, iss *x509.Certificate, withOps ...WithOption) (*base, error) { + if sub == nil { + return nil, errors.Errorf("subject certificate cannot be nil") + } + if iss == nil { + return nil, errors.Errorf("issuing certificate cannot be nil") + } + + var ( + err error + b = &base{} + ) + b.SetSubject(sub) + b.SetIssuer(iss) + + for _, op := range withOps { + if err := op(b); err != nil { + return nil, errors.WithStack(err) + } + } + + if b.SubjectPublicKey() == nil { + if err := b.GenerateDefaultKeyPair(); err != nil { + return nil, errors.WithStack(err) + } + } + + if b.sub.SubjectKeyId == nil { + pubBytes, err := x509.MarshalPKIXPublicKey(b.SubjectPublicKey()) + if err != nil { + return nil, errors.Wrapf(err, "failed to marshal public key to bytes") + } + hash := sha1.Sum(pubBytes) + b.sub.SubjectKeyId = hash[:] // takes slice over the whole array + } + + if b.sub.SerialNumber == nil { + // TODO figure out how to test rand w/out threading as another arg + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + b.sub.SerialNumber, err = rand.Int(rand.Reader, serialNumberLimit) + // TODO error condition untested -- hard to test w/o mocking rand + if err != nil { + return nil, errors.Wrapf(err, "Failed to generate serial number for "+ + "certificate with common name '%s'", sub.Subject.CommonName) + } + } + + return b, nil +} + +func fromBase(profile Profile, b base) { + profile.SetSubject(b.Subject()) + profile.SetIssuer(b.Issuer()) + profile.SetSubjectPublicKey(b.SubjectPublicKey()) + profile.SetSubjectPrivateKey(b.SubjectPrivateKey()) + profile.SetIssuerPrivateKey(b.issPriv) +} + +func (b *base) Issuer() *x509.Certificate { + return b.iss +} + +func (b *base) Subject() *x509.Certificate { + return b.sub +} + +func (b *base) SubjectPrivateKey() interface{} { + return b.subPriv +} + +func (b *base) SubjectPublicKey() interface{} { + return b.subPub +} + +func (b *base) SetIssuer(iss *x509.Certificate) { + b.iss = iss +} + +func (b *base) SetSubject(sub *x509.Certificate) { + b.sub = sub +} + +func (b *base) SetSubjectPrivateKey(priv interface{}) { + b.subPriv = priv +} + +func (b *base) SetIssuerPrivateKey(priv interface{}) { + b.issPriv = priv +} + +func (b *base) SetSubjectPublicKey(pub interface{}) { + b.subPub = pub +} + +func (b *base) GenerateKeyPair(kty, crv string, size int) error { + pub, priv, err := keys.GenerateKeyPair(kty, crv, size) + if err != nil { + return err + } + b.SetSubjectPublicKey(pub) + b.SetSubjectPrivateKey(priv) + return nil +} + +func (b *base) GenerateDefaultKeyPair() error { + pub, priv, err := keys.GenerateDefaultKeyPair() + if err != nil { + return err + } + b.SetSubjectPublicKey(pub) + b.SetSubjectPrivateKey(priv) + return nil +} + +// CreateCertificate creates an x509 Certificate using the configuration stored +// in the profile. +func (b *base) CreateCertificate() ([]byte, error) { + if b.SubjectPublicKey() == nil { + return nil, errors.Errorf("Profile does not have subject public key. Need to call 'profile.GenKeys(...)' or use setters to populate keys") + } + if b.issPriv == nil { + return nil, errors.Errorf("Profile does not have issuer private key. Use setters to populate this field.") + } + return x509.CreateCertificate(rand.Reader, b.Subject(), b.Issuer(), + b.SubjectPublicKey(), b.issPriv) +} + +// Create Certificate from profile and write the certificate and private key +// to disk. +func (b *base) CreateWriteCertificate(crtOut, keyOut, pass string) ([]byte, error) { + crtBytes, err := b.CreateCertificate() + if err != nil { + return nil, err + } + if err := WriteCertificate(crtBytes, crtOut); err != nil { + return nil, err + } + if err := keys.WritePrivateKey(b.SubjectPrivateKey(), pass, keyOut); err != nil { + return nil, err + } + return crtBytes, nil +} diff --git a/crypto/certificates/x509/rootProfile.go b/crypto/certificates/x509/rootProfile.go new file mode 100644 index 00000000..fcb37865 --- /dev/null +++ b/crypto/certificates/x509/rootProfile.go @@ -0,0 +1,80 @@ +package x509 + +import ( + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "time" + + "github.com/pkg/errors" +) + +// Root implements the Profile for a root certificate. +type Root struct { + base +} + +// NewRootProfile returns a new root x509 Certificate profile. +func NewRootProfile(name string, withOps ...WithOption) (*Root, error) { + crt, err := defaultRootTemplate(name) + if err != nil { + return nil, errors.WithStack(err) + } + + b, err := newBase(crt, crt, withOps...) + if err != nil { + return nil, errors.WithStack(err) + } + + r := &Root{} + fromBase(r, *b) + r.SetIssuerPrivateKey(r.SubjectPrivateKey()) + return r, nil +} + +// NewRootProfileWithTemplate returns a new root x509 Certificate profile. +func NewRootProfileWithTemplate(crt *x509.Certificate, withOps ...WithOption) (*Root, error) { + b, err := newBase(crt, crt, withOps...) + if err != nil { + return nil, err + } + + r := &Root{} + fromBase(r, *b) + r.SetIssuerPrivateKey(r.SubjectPrivateKey()) + return r, nil +} + +func defaultRootTemplate(cn string) (*x509.Certificate, error) { + var ( + err error + notBefore = time.Now() + ) + + ct := &x509.Certificate{ + IsCA: true, + NotBefore: notBefore, + // 10 year root certificate validity. + NotAfter: notBefore.Add(time.Hour * 24 * 365 * 10), + KeyUsage: x509.KeyUsageKeyEncipherment | + x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + MaxPathLen: 1, + MaxPathLenZero: false, + Issuer: pkix.Name{CommonName: cn}, + Subject: pkix.Name{CommonName: cn}, + } + + // TODO figure out how to test rand w/out threading as another arg + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + ct.SerialNumber, err = rand.Int(rand.Reader, serialNumberLimit) + // TODO error condition untested -- hard to test w/o mocking rand + if err != nil { + return nil, errors.Wrapf(err, "Failed to generate serial number for "+ + "certificate with common name '%s'", cn) + } + + return ct, nil + +} diff --git a/crypto/certificates/x509/test_files/badca.crt b/crypto/certificates/x509/test_files/badca.crt new file mode 100644 index 00000000..df88045a --- /dev/null +++ b/crypto/certificates/x509/test_files/badca.crt @@ -0,0 +1,37 @@ +-----BEGIN CERTIFICATE----- +MIIGcDCCBFigAwIBAgIBATANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJVUzEL +MAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoMCXNt +YWxsc3RlcDEfMB0GA1UEAwwWaW50ZXJuYWwuc21hbGxzdGVwLmNvbTAeFw0xNzA2 +MTQyMTU3MTRaFw0yNzA2MTIyMTU3MTRaMGcxHzAdBgNVBAMMFmludGVybmFsLnNt +YWxsc3RlcC5jb20xCzAJBgNVBAYTAlVTMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2Nv +MRIwEAYDVQQKDAlzbWFsbHN0ZXAxCzAJBgNVBAgMAkNBMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAt0M+Eu4i6hdERJ8yTrVwZz7Hds2WQrPd89NuVYP4 +ouJbuG+udcGeWpJc8nIlv0w6F/8CRMxuY9FaHY2epiGb4Xw9P1a+XXIm70OpQFEQ +VPJycbHyphLuGjSq3Mc3+AVKDyQZSYuW+C6XJYsI06p2Y0xm8ST2BWc7b4CuNVte +e38eVaxiISs5P7GIcD6nMhTbEtC6H6j+EjEcZOboMFNPQNRXlPdtyrLT4QhDSLGi +d/vTs33ttkv0ebUuCFGc/965KmP1NawKhka3x0kJ20/vXzM8XjwLSOG2041+MdnJ +rgCllQkirbTBfIg0u64yDV81k853RF9ZvkuNI/FPQ5ueo1HeB2ExumQl3S6vT+nv +TgoDLkwq6kgoMYXPjO4kDnklf3xiCVfmsMUmGgOYbFcRD5e4fTOHeBRGC27aklOI +k5CODMcFQRvHliQZpnORWmNgzvaNLEOf5d3wk613+rj+iru//EeuMNVP+PO6k95/ +g+D6nEvc9WUjiO/2vDR4Dw+LiAeWH+bXx+AVCugin7snkz0xmNUtvzne2LLqUTie +s3mRxw87pjgDvTDstF7il3D83dxzJI7xp/00WlMdzwaxPT182Y45JH6mS0BNtUEo +oPtfDtyE54SXa56R+tZREzw+CizAbS4FzlwQmYY5pZHuSQKAxQMFOJPbFTrEt8tk +Tg0CAwEAAaOCASUwggEhMIGZBgNVHSMEgZEwgY6AFIuhuVEao1ML4RaiJbaSFlHs +fDgloWukaTBnMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNh +biBGcmFuY2lzY28xEjAQBgNVBAoMCXNtYWxsc3RlcDEfMB0GA1UEAwwWaW50ZXJu +YWwuc21hbGxzdGVwLmNvbYIJANHnRMPsmOa0MBIGA1UdEwEB/wQIMAYBAf8CAQAw +HQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDwEB/wQEAwIBRjAh +BgNVHREEGjAYghZpbnRlcm5hbC5zbWFsbHN0ZXAuY29tMB0GA1UdDgQWBBRFga2r +OHyWFPOHK2hrFPP7PKx28DANBgkqhkiG9w0BAQsFAAOCAgEAay1ZCyfCJNwmGawA +pWO8pUsmv39dUu6lfeG0yHBGQMUUrmuUrn1JbAQQ5rxXSIMWQHNskACO37l5Ol0+ +UZNjo+2vnDS6+5XQJ2k68e9Ivb/dm7nrJfDrs+WcLcQ/fCn01pKA02gIESQzIHci +zuSAtj8iVG7IkfWkGAK7DLsrktAcB+2+8SqzN1XwLlb2jkPOuseM+BidesvP+cQR +ik614DwNAHkD5v+n6Ee2LrDFu7Qsl2fNe3OPIMe9cWFC68yL/bhXpeTYzfArHVbR +imZvEB6e6wvW0J4o+e25LIFIavLuoLTt0Js+3Q2XD/HNqxBJsZe6y/LX1JMGLEe1 +pWOcGt97xXL4hdqWT+M4kUgpbxmuEEretlVVydpKncxCa/ovzWyhp4TJ4RZ0J67p +vUbVV+BiW3nfIbz+3pS9a0O4huvp+iNfIKo74xLYWGcT/37Le/X7nJbwLLg+fzCj +5CKgEj6YWJ5om9fD5aV3CLczsFsSkzg0R292DPYuWiWAcgPrrVa2mSaNSWacajmx +5CIBu5G/VBmGjYSHa+dnagPFExsDpksB6AzX82pthDewAikVSg3DTK4sDuKXpXFS +CMRrIxHUYRUgyoMjsNUR9KQeq7hiTwCq325KpL7XWBtRCoBOwLWX1lECScJITQA4 +XevZD8LWagqK8/3ZQsrBDmcmrAs+ +-----END CERTIFICATE----- diff --git a/crypto/certificates/x509/test_files/badca.key b/crypto/certificates/x509/test_files/badca.key new file mode 100644 index 00000000..74aae1d2 --- /dev/null +++ b/crypto/certificates/x509/test_files/badca.key @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,6D4455D3019FC904 + +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 +-----END RSA PRIVATE KEY----- diff --git a/crypto/certificates/x509/test_files/badcsr.csr b/crypto/certificates/x509/test_files/badcsr.csr new file mode 100644 index 00000000..6a2820b2 --- /dev/null +++ b/crypto/certificates/x509/test_files/badcsr.csr @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE REQUEST----- +LIIDNjCCAh4CAQAwYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQH +DA1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKDAlzbWFsbHN0ZXAxGzAZBgNVBAMMEnRl +c3Quc21hbGxzdGVwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANPahliigZ38QpBLmQMS3MVKKZ5gapNjqR7LIEYoYWa4lTFiUnbwg8tSfIFcgLZr +jNIxn7/98+JOJHKgS03NhFJoS5hej0LyypleOGJ0nk2qawYVKnn1ftoKjkfxkfZI +a/5rsDF1jhNBspB/KPHWE0eimKQJbUiVG1zA1sExnXDecF3vJfBj+DPDWngx4yxR +/jYEKjt4tQ6Ei752TbosrCHYeYXzkr6iAwiNz6vT/ewLb6b8JmuN8X6Y1I9ogDGx +hntBJ1jAK8x3IGTjYbkm+mqVuCyhNcHtGfEHcBnUEzLAPrVFn8kGiAnU17FJ0uQ7 +1C9CtUzgBRZCxSBm6Qs+Zs8CAwEAAaCBjTCBigYJKoZIhvcNAQkOMX0wezAMBgNV +HRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAOBgNVHQ8B +Af8EBAMCBaAwHQYDVR0RBBYwFIISdGVzdC5zbWFsbHN0ZXAuY29tMB0GA1UdDgQW +BBQj6N4RTAAjhV3UBYXH72mkdOGpqzANBgkqhkiG9w0BAQsFAAOCAQEAN0/ivCBk +FD53SqtRmqqc7C9saoRNvV+wDi4Sg6YGLFQLjbZPJrqQURWdHtV9O3sb3p8O5erX +9Kgq3C7fqd//0mro4GZ1GTpjsPKIMocZFfH7zEhAZlvQLRKWICjoBaOwxQum2qY/ +B3+ltAXb4uqGdbI0jPkkyWGN5CQhK+ZHoYe/zGtTEmHBcPxRtJJkukQQjUgZhjU2 +Z7K+w3AjOxj47XLNHHlW83QYUJ2mN+mEZF9DhrZb2ydYOlpy0V2NJwv7QrmnFaDj +R0v3BFLTblIp100li3oV2QaM/yESrgo9XIjEEGzCGz5cNs5ovNadufUZDCJyyT4q +ZEp7knvU2osWRw== +-----END CERTIFICATE REQUEST----- diff --git a/crypto/certificates/x509/test_files/badpem.crt b/crypto/certificates/x509/test_files/badpem.crt new file mode 100644 index 00000000..e208eaa4 --- /dev/null +++ b/crypto/certificates/x509/test_files/badpem.crt @@ -0,0 +1,37 @@ +-----BEGIN CERTIFICATE----- +MIIGcDCCBFigAwIBAgIBATANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJVUzEL +MAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoMCXNt +YWxsc3RlcDEfMB0GA1UEAwwWaW50ZXJuYWwuc21hbGxzdGVwLmNvbTAeFw0xNzA2 +MTQyMTU3MTRaFw0yNzA2MTIyMTU3MTRaMGcxHzAdBgNVBAMMFmludGVybmFsLnNt +YWxsc3RlcC5jb20xCzAJBgNVBAYTAlVTMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2Nv +MRIwEAYDVQQKDAlzbWFsbHN0ZXAxCzAJBgNVBAgMAkNBMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAt0M+Eu4i6hdERJ8yTrVwZz7Hds2WQrPd89NuVYP4 +ouJbuG+udcGeWpJc8nIlv0w6F/8CRMxuY9FaHY2epiGb4Xw9P1a+XXIm70OpQFEQ +VPJycbHyphLuGjSq3Mc3+AVKDyQZSYuW+C6XJYsI06p2Y0xm8ST2BWc7b4CuNVte +e38eVaxiISs5P7GIcD6nMhTbEtC6H6j+EjEcZOboMFNPQNRXlPdtyrLT4QhDSLGi +d/vTs33ttkv0ebUuCFGc/965KmP1NawKhka3x0kJ20/vXzM8XjwLSOG2041+MdnJ +rgCllQkirbTBfIg0u64yDV81k853RF9ZvkuNI/FPQ5ueo1HeB2ExumQl3S6vT+nv +TgoDLkwq6kgoMYXPjO4kDnklf3xiCVfmsMUmGgOYbFcRD5e4fTOHeBRGC27aklOI +k5CODMcFQRvHliQZpnORWmNgzvaNLEOf5d3wk613+rj+iru//EeuMNVP+PO6k95/ +g+D6nEvc9WUjiO/2vDR4Dw+LiAeWH+bXx+AVCugin7snkz0xmNUtvzne2LLqUTie +s3mRxw87pjgDvTDstF7il3D83dxzJI7xp/00WlMdzwaxPT182Y45JH6mS0BNtUEo +oPtfDtyE54SXa56R+tZREzw+CizAbS4FzlwQmYY5pZHuSQKAxQMFOJPbFTrEt8tk +Tg0CAwEAAaOCASUwggEhMIGZBgNVHSMEgZEwgY6AFIuhuVEao1ML4RaiJbaSFlHs +fDgloWukaTBnMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNh +biBGcmFuY2lzY28xEjAQBgNVBAoMCXNtYWxsc3RlcDEfMB0GA1UEAwwWaW50ZXJu +YWwuc21hbGxzdGVwLmNvbYIJANHnRMPsmOa0MBIGA1UdEwEB/wQIMAYBAf8CAQAw +HQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDwEB/wQEAwIBRjAh +BgNVHREEGjAYghZpbnRlcm5hbC5zbWFsbHN0ZXAuY29tMB0GA1UdDgQWBBRFga2r +OHyWFPOHK2hrFPP7PKx28DANBgkqhkiG9w0BAQsFAAOCAgEAay1ZCyfCJNwmGawA +pWO8pUsmv39dUu6lfeG0yHBGQMUUrmuUrn1JbAQQ5rxXSIMWQHNskACO37l5Ol0+ +UZNjo+2vnDS6+5XQJ2k68e9Ivb/dm7nrJfDrs+WcLcQ/fCn01pKA02gIESQzIHci +zuSAtj8iVG7IkfWkGAK7DLsrktAcB+2+8SqzN1XwLlb2jkPOuseM+BidesvP+cQR +ik614DwNAHkD5v+n6Ee2LrDFu7Qsl2fNe3OPIMe9cWFC68yL/bhXpeTYzfArHVbR +imZvEB6e6wvW0J4o+e25LIFIavLuoLTt0Js+3Q2XD/HNqxBJsZe6y/LX1JMGLEe1 +pWOcGt97xXL4hdqWT+M4kUgpbxmuEEretlVVydpKncxCa/ovzWyhp4TJ4RZ0J67p +vUbVV+BiW3nfIbz+3pS9a0O4huvp+iNfIKo74xLYWGcT/37Le/X7nJbwLLg+fzCj +5CKgEj6YWJ5om9fD5aV3CLczsFsSkzg0R292DPYuWiWAcgPrrVa2mSaNSWacajmx +5CIBu5G/VBmGjYSHa+dnagPFExsDpksB6AzX82pthDewAikVSg3DTK4sDuKXpXFS +CMRrIxHUYRUgyoMjsNUR9KQeq7hiTwCq325KpL7XWBtRCoBOwLWX1lECScJITQA4 +XevZD8LWagqK8/3ZQsrBDmcmr +-----END CERTIFICATE----- diff --git a/crypto/certificates/x509/test_files/badpem.csr b/crypto/certificates/x509/test_files/badpem.csr new file mode 100644 index 00000000..24940b82 --- /dev/null +++ b/crypto/certificates/x509/test_files/badpem.csr @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIDNjCCAh4CAQAwYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQH +DA1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKDAlzbWFsbHN0ZXAxGzAZBgNVBAMMEnRl +c3Quc21hbGxzdGVwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANPahliigZ38QpBLmQMS3MVKKZ5gapNjqR7LIEYoYWa4lTFiUnbwg8tSfIFcgLZr +jNIxn7/98+JOJHKgS03NhFJoS5hej0LyypleOGJ0nk2qawYVKnn1ftoKjkfxkfZI +a/5rsDF1jhNBspB/KPHWE0eimKQJbUiVG1zA1sExnXDecF3vJfBj+DPDWngx4yxR +/jYEKjt4tQ6Ei752TbosrCHYeYXzkr6iAwiNz6vT/ewLb6b8JmuN8X6Y1I9ogDGx +hntBJ1jAK8x3IGTjYbkm+mqVuCyhNcHtGfEHcBnUEzLAPrVFn8kGiAnU17FJ0uQ7 +1C9CtUzgBRZCxSBm6Qs+Zs8CAwEAAaCBjTCBigYJKoZIhvcNAQkOMX0wezAMBgNV +HRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAOBgNVHQ8B +Af8EBAMCBaAwHQYDVR0RBBYwFIISdGVzdC5zbWFsbHN0ZXAuY29tMB0GA1UdDgQW +BBQj6N4RTAAjhV3UBYXH72mkdOGpqzANBgkqhkiG9w0BAQsFAAOCAQEAN0/ivCBk +FD53SqtRmqqc7C9saoRNvV+wDi4Sg6YGLFQLjbZPJrqQURWdHtV9O3sb3p8O5erX +9Kgq3C7fqd//0mro4GZ1GTpjsPKIMocZFfH7zEhAZlvQLRKWICjoBaOwxQum2qY/ +B3+ltAXb4uqGdbI0jPkkyWGN5CQhK+ZHoYe/zGtTEmHBcPxRtJJkukQQjUgZhjU2 +Z7K+w3AjOxj47XLNHHlW83QYUJ2mN+mEZF9DhrZb2ydYOlpy0V2NJwv7QrmnFaDj +R0v3BFLTblIp100li3oV2QaM/yESrgo9XIjEEGzCGz5cNs5ovNadufUZDCJyyT4q +ZEp7knvU2osWR +-----END CERTIFICATE REQUEST----- diff --git a/crypto/certificates/x509/test_files/badpem.key b/crypto/certificates/x509/test_files/badpem.key new file mode 100644 index 00000000..a31402a3 --- /dev/null +++ b/crypto/certificates/x509/test_files/badpem.key @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,6D4455D3019FC904 + +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 +-----END RSA PRIVATE KEY----- diff --git a/crypto/certificates/x509/test_files/badsig.csr b/crypto/certificates/x509/test_files/badsig.csr new file mode 100644 index 00000000..bb733121 --- /dev/null +++ b/crypto/certificates/x509/test_files/badsig.csr @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIDNjCCAh4CAQAwYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQH +DA1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKDAlzbWFsbHN0ZXAxGzAZBgNVBAMMEnRl +c3Quc21hbGxzdGVwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANPahliigZ38QpBLmQMS3MVKKZ5gapNjqR7LIEYoYWa4lTFiUnbwg8tSfIFcgLZr +jNIxn7/98+JOJHKgS03NhFJoS5hej0LyypleOGJ0nk2qawYVKnn1ftoKjkfxkfZI +a/5rsDF1jhNBspB/KPHWE0eimKQJbUiVG1zA1sExnXDecF3vJfBj+DPDWngx4yxR +/jYEKjt4tQ6Ei752TbosrCHYeYXzkr6iAwiNz6vT/ewLb6b8JmuN8X6Y1I9ogDGx +hntBJ1jAK8x3IGTjYbkm+mqVuCyhNcHtGfEHcBnUEzLAPrVFn8kGiAnU17FJ0uQ7 +1C9CtUzgBRZCxSBm6Qs+Zs8CAwEAAaCBjTCBigYJKoZIhvcNAQkOMX0wezAMBgNV +HRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAOBgNVHQ8B +Af8EBAMCBaAwHQYDVR0RBBYwFIISdGVzdC5zbWFsbHN0ZXAuY29tMB0GA1UdDgQW +BBQj6N4RTAAjhV3UBYXH72mkdOGpqzANBgkqhkiG9w0BAQsFAAOCAQEAN0/ivCBk +FD53SqtRmqqc7C9saoRNvV+wDi4Sg6YGLFQLjbZPJrqQURWdHtV9O3sb3p8O5erX +9Kgq3C7fqd//0mro4GZ1GTpjsPKIMocZFfH7zEhAZlvQLRKWICjoBaOwxQum2qY/ +B3+ltAXb4uqGdbI0jPkkyWGN5CQhK+ZHoYe/zGtTEmHBcPxRtJJkukQQjUgZhjU2 +Z7K+w3AjOxj47XLNHHlW83QYUJ2mN+mEZF9DhrZb2ydYOlpy0V2NJwv7QrmnFaDj +R0v3BFLTblIp100li3oV2QaM/yESrgo9XIjEEGzCGz5cNs5ovNadufUZDCJyyT4q +ZEp7knvU2psWRw== +-----END CERTIFICATE REQUEST----- diff --git a/crypto/certificates/x509/test_files/ca.crt b/crypto/certificates/x509/test_files/ca.crt new file mode 100644 index 00000000..1fa1e1ce --- /dev/null +++ b/crypto/certificates/x509/test_files/ca.crt @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIRAL4t3Jo++cwAle8DdXchv/owDQYJKoZIhvcNAQELBQAw +WzEMMAoGA1UEBhMDVVNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRIwEAYDVQQK +EwlzbWFsbHN0ZXAxHzAdBgNVBAMTFmludGVybmFsLnNtYWxsc3RlcC5jb20wHhcN +MTcwOTIzMDczNTA3WhcNMTgwOTIzMDczNTA3WjBbMQwwCgYDVQQGEwNVU0ExFjAU +BgNVBAcTDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoTCXNtYWxsc3RlcDEfMB0GA1UE +AxMWaW50ZXJuYWwuc21hbGxzdGVwLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAKA+760g0MbZpFCgG6NpzRh0B8ElgQUteMjynL8ge+r8QsFCm2XY +P7BYzjyyD9FdNTRw2toUB8G/t3E5jhjrE6qvG0PWsluzFEtfh0uS59BPS6YTgurY +LE3PAc/+fCxEI3SfA4TCYVnzUcSkhcHNT0PtMWG8tR7S+0GFc1O22wUn2e/dKK1d +fCGhEu9gzuA3TjJgpzfmXTBFUijiIRSaXHiYcUWR0FE3CKVULlM2jJ/uxXZr6kSZ +STxQ/kisaIzOe7Y/uA9F4fyfCHdaCsvkv3d11d1SkOdBCY+jx+PG5uLDWxGCgZYZ +dWDjOX43gquSaC3bFMi+cglF4Wx+n173elcOuoF77bVNBOOtWIbWNLYVujkvbzec +Dn0NLySl79OKMuSuF995iR7Or29gcbaZz5j1NHeqbhb24HWZ+9xi3ws4ike7GZ5Q +akZ3AwEcwVwbMhQ5KCoWKroSWpYUvQ58PGgy+ml5f42Cjg/e1nH1/hpnqwzzItbs +6qb9I0RV12Y6KCEqmKIrs1EdHc351aknhiZ1Zgdankhym3TiAo08mkDIqbJUKjR+ +0De7ynBKBDq79NWfb5DLdXH95z1DDZvI4FJ9X0eAlo3DbkZXFIfoeF1gL577pEES +NXZKXqmY2hPcsZUKAhXIEK3zmNXJVGeqb9sNnYtBTY6zBo5sA+40WDHNAgMBAAGj +gakwgaYwDgYDVR0PAQH/BAQDAgGmMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF +BQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRVBozFxzNJ9pSzW4lW +MZF/q+6SPTAfBgNVHSMEGDAWgBTd1DpE9Y4R5OoP7f5WT91mPCU2gDAhBgNVHREE +GjAYghZpbnRlcm5hbC5zbWFsbHN0ZXAuY29tMA0GCSqGSIb3DQEBCwUAA4ICAQCj +3CT2xk9xLFTX0Ki30PWB6/0OxN5L0sKk7pJAWIzgdsKYrBbh93sA/oYGsnr0iW6F +VLWkvqMmGLlp5yLg6LIQaed5C26u2fc0udzXTVfEx7QjOtLtetLt7LQ6Kzb7FOri +iiUfvilLttXyESQ3WzlCTh4OlrIhNWg1w56jc0/7GAJY0LTrsCoYOSwR2qlBQiTI +41+fApAlRZKI0eNP9X4GxVkgh+wIVuF4zXr/460VkaWT9RquvS2MIaotdZ3IBTTk +tCmFHvbI/eZOWo1KbjPdSByOZVI1gBfpU/eufsdysRebZwBsYRDF391QJ3aKt2cZ +WnAjtYl3lfcXA/iFj2HL04vmdTweBVvl7Wa2EsM/iiEhPOWlIXdQx81FURE8nc2H +DCQJgQIbwqZ4LQJrmF6tmzhmJUH2/9Vxc/rYMSx6NgT6sSoz+gXt0yDd20tF7SU6 +smiL/uCGfSXAbqsI+MO8Nc7gOPhKtHeW4r2Kx/OzuFkYAFBez0GqxmnJ/xKgwfGO +v+pzRC09KUgpncGZuB6S9PUWPhC15LO5bFBF1tiUy8hyzzbopfyPtLnhY8tq4u+j +lGTAz5g+7chL+j6UqZZYGD5DqIiOYO/YK0wi92ov1wu6Pkvb33dy1LVWJaGjcAQv +7cuffXbN9jXxAtrFxrmY3qfXnUR4K2lCESWPjCCmaw== +-----END CERTIFICATE----- diff --git a/crypto/certificates/x509/test_files/ca.key b/crypto/certificates/x509/test_files/ca.key new file mode 100644 index 00000000..85e782cb --- /dev/null +++ b/crypto/certificates/x509/test_files/ca.key @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,36945dfbb2645820d163fad834a3d290 + +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 +-----END RSA PRIVATE KEY----- diff --git a/crypto/certificates/x509/test_files/noPasscodeBadCa.key b/crypto/certificates/x509/test_files/noPasscodeBadCa.key new file mode 100644 index 00000000..90caebe6 --- /dev/null +++ b/crypto/certificates/x509/test_files/noPasscodeBadCa.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/crypto/certificates/x509/test_files/noPasscodeCa.crt b/crypto/certificates/x509/test_files/noPasscodeCa.crt new file mode 100644 index 00000000..f83c9bf5 --- /dev/null +++ b/crypto/certificates/x509/test_files/noPasscodeCa.crt @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/crypto/certificates/x509/test_files/noPasscodeCa.key b/crypto/certificates/x509/test_files/noPasscodeCa.key new file mode 100644 index 00000000..056875b5 --- /dev/null +++ b/crypto/certificates/x509/test_files/noPasscodeCa.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/crypto/certificates/x509/test_files/test.smallstep.com.csr b/crypto/certificates/x509/test_files/test.smallstep.com.csr new file mode 100644 index 00000000..5e85b727 --- /dev/null +++ b/crypto/certificates/x509/test_files/test.smallstep.com.csr @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIDNjCCAh4CAQAwYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQH +DA1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKDAlzbWFsbHN0ZXAxGzAZBgNVBAMMEnRl +c3Quc21hbGxzdGVwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANPahliigZ38QpBLmQMS3MVKKZ5gapNjqR7LIEYoYWa4lTFiUnbwg8tSfIFcgLZr +jNIxn7/98+JOJHKgS03NhFJoS5hej0LyypleOGJ0nk2qawYVKnn1ftoKjkfxkfZI +a/5rsDF1jhNBspB/KPHWE0eimKQJbUiVG1zA1sExnXDecF3vJfBj+DPDWngx4yxR +/jYEKjt4tQ6Ei752TbosrCHYeYXzkr6iAwiNz6vT/ewLb6b8JmuN8X6Y1I9ogDGx +hntBJ1jAK8x3IGTjYbkm+mqVuCyhNcHtGfEHcBnUEzLAPrVFn8kGiAnU17FJ0uQ7 +1C9CtUzgBRZCxSBm6Qs+Zs8CAwEAAaCBjTCBigYJKoZIhvcNAQkOMX0wezAMBgNV +HRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAOBgNVHQ8B +Af8EBAMCBaAwHQYDVR0RBBYwFIISdGVzdC5zbWFsbHN0ZXAuY29tMB0GA1UdDgQW +BBQj6N4RTAAjhV3UBYXH72mkdOGpqzANBgkqhkiG9w0BAQsFAAOCAQEAN0/ivCBk +FD53SqtRmqqc7C9saoRNvV+wDi4Sg6YGLFQLjbZPJrqQURWdHtV9O3sb3p8O5erX +9Kgq3C7fqd//0mro4GZ1GTpjsPKIMocZFfH7zEhAZlvQLRKWICjoBaOwxQum2qY/ +B3+ltAXb4uqGdbI0jPkkyWGN5CQhK+ZHoYe/zGtTEmHBcPxRtJJkukQQjUgZhjU2 +Z7K+w3AjOxj47XLNHHlW83QYUJ2mN+mEZF9DhrZb2ydYOlpy0V2NJwv7QrmnFaDj +R0v3BFLTblIp100li3oV2QaM/yESrgo9XIjEEGzCGz5cNs5ovNadufUZDCJyyT4q +ZEp7knvU2osWRw== +-----END CERTIFICATE REQUEST----- diff --git a/crypto/certificates/x509/types.go b/crypto/certificates/x509/types.go new file mode 100644 index 00000000..8201bf7d --- /dev/null +++ b/crypto/certificates/x509/types.go @@ -0,0 +1,111 @@ +package x509 + +import ( + "crypto/tls" + "fmt" + + "github.com/pkg/errors" +) + +// ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer +// x509 Certificate blocks. +type ASN1DN struct { + Country string `step:"country"` + Organization string `step:"organization"` + OrganizationalUnit string `step:"organizationalUnit"` + Locality string `step:"locality"` + Province string `step:"province"` + StreetAddress string `step:"streetAddress"` + CommonName string `step:"commonName"` +} + +// TLSVersion represents a TLS version number. +type TLSVersion float64 + +// Validate implements models.Validator and checks that a cipher suite is +// valid. +func (v TLSVersion) Validate() error { + if _, ok := tlsVersions[v]; ok { + return nil + } + return errors.Errorf("%f is not a valid tls version", v) +} + +// Value returns the Go constant for the TLSVersion. +func (v TLSVersion) Value() uint16 { + return tlsVersions[v] +} + +// String returns the Go constant for the TLSVersion. +func (v TLSVersion) String() string { + k := v.Value() + switch k { + case tls.VersionTLS10: + return "1.0" + case tls.VersionTLS11: + return "1.1" + case tls.VersionTLS12: + return "1.2" + default: + return fmt.Sprintf("unexpected value: %d", k) + } +} + +// tlsVersions has the list of supported tls version. +var tlsVersions = map[TLSVersion]uint16{ + // Defaults to TLS 1.2 + 0: tls.VersionTLS12, + // Options + 1.0: tls.VersionTLS10, + 1.1: tls.VersionTLS11, + 1.2: tls.VersionTLS12, +} + +// CipherSuites represents an array of string codes representing the cipher +// suites. +type CipherSuites []string + +// Validate implements models.Validator and checks that a cipher suite is +// valid. +func (c CipherSuites) Validate() error { + for _, s := range c { + if _, ok := cipherSuites[s]; !ok { + return errors.Errorf("%s is not a valid cipher suite", s) + } + } + return nil +} + +// Value returns an []uint16 for the cipher suites. +func (c CipherSuites) Value() []uint16 { + values := make([]uint16, len(c)) + for i, s := range c { + values[i] = cipherSuites[s] + } + return values +} + +// cipherSuites has the list of supported cipher suites. +var cipherSuites = map[string]uint16{ + "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, + "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, + "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, +} diff --git a/crypto/keys/clean_test.go b/crypto/keys/clean_test.go new file mode 100644 index 00000000..219bc2dd --- /dev/null +++ b/crypto/keys/clean_test.go @@ -0,0 +1,26 @@ +package keys + +import ( + "io/ioutil" + "log" + "os" + "testing" +) + +func TestMain(m *testing.M) { + // discard log output when testing + log.SetOutput(ioutil.Discard) + + result := m.Run() + + clean := func(files []string) { + for _, f := range files { + if _, err := os.Stat(f); !os.IsNotExist(err) { + os.Remove(f) + } + } + } + clean([]string{"./test.key"}) + + os.Exit(result) +} diff --git a/crypto/keys/key.go b/crypto/keys/key.go new file mode 100644 index 00000000..0869f12d --- /dev/null +++ b/crypto/keys/key.go @@ -0,0 +1,311 @@ +package keys + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "math/big" + "os" + + "github.com/pkg/errors" + "golang.org/x/crypto/ed25519" +) + +// DefaultPEMCipher is the default algorithm used when encrypting PEM blocks +// by the CA. +var ( + DefaultPEMCipher = x509.PEMCipherAES128 + // DefaultKeyType is the default type of a private key. + DefaultKeyType = "EC" + // DefaultKeySize is the default size (in # of bits) of a private key. + DefaultKeySize = 2048 + // DefaultKeyCurve is the default curve of a private key. + DefaultKeyCurve = "P-256" + + x509EncryptPEMBlock = x509.EncryptPEMBlock + x509MarshalECPrivateKey = x509.MarshalECPrivateKey +) + +// PublicKey extracts a public key from a private key. +func PublicKey(priv interface{}) (interface{}, error) { + switch k := priv.(type) { + case *rsa.PrivateKey: + return &k.PublicKey, nil + case *ecdsa.PrivateKey: + return &k.PublicKey, nil + default: + return nil, errors.Errorf("unrecognized key type: %T", priv) + } +} + +// PrivatePEM will convert a private key and encryption password +// to an encrypted pem block. +// Nil encryption options is identical to InsecureEncOpts() -- so no encryption. +func PrivatePEM(priv interface{}, e *EncOpts) (*pem.Block, error) { + var insecure bool + if e == nil { + e = InsecureEncOpts() + } + if e.pass == "" { + insecure = true + } + switch k := priv.(type) { + case *rsa.PrivateKey: + if insecure { + return &pem.Block{ + Bytes: x509.MarshalPKCS1PrivateKey(k), + Type: "RSA PRIVATE KEY", + }, nil + } + return x509EncryptPEMBlock(rand.Reader, "RSA PRIVATE KEY", + x509.MarshalPKCS1PrivateKey(k), []byte(e.pass), e.cipher) + case *ecdsa.PrivateKey: + b, err := x509MarshalECPrivateKey(k) + if err != nil { + return nil, errors.Wrap(err, "Unable to marshal EC private key") + } + if insecure { + return &pem.Block{ + Bytes: b, + Type: "EC PRIVATE KEY", + }, nil + } + return x509EncryptPEMBlock(rand.Reader, "EC PRIVATE KEY", + b, []byte(e.pass), e.cipher) + default: + return nil, errors.Errorf("Unrecognized key - type: %T, value: %v", priv, priv) + } +} + +// EncOpts is a type containing options for encrypting secrets to disk. +// The fields are a password and cipher used for encryption. +type EncOpts struct { + pass string + cipher x509.PEMCipher +} + +// InsecureEncOpts returns an empty EncOpts. Unspecified password indicates insecure. +func InsecureEncOpts() *EncOpts { + return &EncOpts{} +} + +// DefaultEncOpts populates the password field and sets a sane default +// for the encryption cipher. +func DefaultEncOpts(pass string) *EncOpts { + return &EncOpts{pass, x509.PEMCipherAES128} +} + +// PublicPEM returns the public key in PEM block format. +func PublicPEM(pub interface{}) (*pem.Block, error) { + pubBytes, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return nil, errors.WithStack(err) + } + + return &pem.Block{ + Bytes: pubBytes, + Type: "PUBLIC KEY", + }, nil +} + +// GenerateDefaultKey generates a public/private key pair using sane defaults +// for key type, curve, and size. +func GenerateDefaultKey() (interface{}, error) { + return GenerateKey(DefaultKeyType, DefaultKeyCurve, DefaultKeySize) +} + +// GenerateKey generates a key of the given type (kty). +func GenerateKey(kty, crv string, size int) (interface{}, error) { + switch kty { + case "EC": + return generateECKey(crv) + case "RSA": + return generateRSAKey(size) + case "OKP": + return generateOKPKey(crv) + case "oct": + return generateOctKey(size) + default: + return nil, errors.Errorf("unrecognized key type: %s", kty) + } +} + +func generateECKey(crv string) (interface{}, error) { + var c elliptic.Curve + switch crv { + case "P-256": + c = elliptic.P256() + case "P-384": + c = elliptic.P384() + case "P-521": + c = elliptic.P521() + default: + return nil, errors.Errorf("invalid value for argument crv (crv: '%s')", crv) + } + + key, err := ecdsa.GenerateKey(c, rand.Reader) + if err != nil { + return nil, errors.Wrap(err, "error generating EC key") + } + + return key, nil +} + +func generateRSAKey(bits int) (interface{}, error) { + key, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, errors.Wrap(err, "error generating RSA key") + } + + return key, nil +} + +func generateOKPKey(crv string) (interface{}, error) { + switch crv { + case "Ed25519": + _, key, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, errors.Wrap(err, "error generating Ed25519 key") + } + + return key, nil + default: + return nil, errors.Errorf("missing or invalid value for argument 'crv'. "+ + "expected 'Ed25519', but got '%s'", crv) + } +} + +func generateOctKey(size int) (interface{}, error) { + const chars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + result := make([]byte, size) + for i := range result { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars)))) + if err != nil { + return nil, err + } + result[i] = chars[num.Int64()] + } + return result, nil +} + +// LoadPrivateKey loads a private key from a file. +// The first argument is the ASN.1 DER formatted private key. +// The second argument is a function that returns an encryption passphrase. If +// the private key is not encrypted the second arg can be nil or simply return +// an empty string. If the private key is encrypted then `getPass` should return +// the decryptor. +func LoadPrivateKey(bytes []byte, getPass func() (string, error)) (interface{}, error) { + p, _ := pem.Decode(bytes) + if p == nil { + return nil, errors.Errorf("invalid key - key is not PEM formatted") + } + + // The following block is focused on getting the decrypted key bytes + // from the PEM block. + // 1. Check if the key bytes were encrypted. + // a) encrypted: go to 2. + // b) not encrypted: hakuna mata (we don't have to do much). + // 2. The key bytes are encrypted so we need a key to decrypt them. + // a) `pass` is empty therefore we request a decryption passphrase from + // stdin. + // 3. Decrypt the key bytes using either a password from stdin or one + // passed in as an agument. + var der []byte + if x509.IsEncryptedPEMBlock(p) { + if getPass == nil { + return nil, errors.Errorf("private key needs a decryption passphrase") + } + pass, err := getPass() + if err != nil { + return nil, err + } + der, err = x509.DecryptPEMBlock(p, []byte(pass)) + if err != nil { + return nil, errors.WithStack(err) + } + } else { + der = p.Bytes + } + + var ( + err error + key interface{} + ) + switch p.Type { + case "RSA PRIVATE KEY": + key, err = x509.ParsePKCS1PrivateKey(der) + if err != nil { + return nil, errors.Wrapf(err, "error parsing RSA key") + } + case "EC PRIVATE KEY": + key, err = x509.ParseECPrivateKey(der) + if err != nil { + return nil, errors.Wrapf(err, "error parsing EC key") + } + default: + return nil, errors.Errorf("unexpected key type: %s", p.Type) + } + return key, nil +} + +// WritePrivateKey encodes a crypto private key to a file on disk in PEM format. +// Any file with the same name will be overwritten. +func WritePrivateKey(key interface{}, pass, out string) error { + // Remove any file with same name, if it exists. + // Permissions on private key files may be such that overwriting them is impossible. + if _, err := os.Stat(out); err == nil { + if err = os.Remove(out); err != nil { + return errors.WithStack(err) + } + } + keyOut, err := os.OpenFile(out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, + os.FileMode(0600)) + if err != nil { + return errors.Wrapf(err, + "failed to open '%s' for writing", out) + } + privPem, err := PrivatePEM(key, DefaultEncOpts(pass)) + if err != nil { + return errors.Wrap(err, + "failed to convert private key to PEM block") + } + err = pem.Encode(keyOut, privPem) + if err != nil { + return errors.Wrapf(err, + "pem encode '%s' failed", out) + } + keyOut.Close() + return nil +} + +// WritePublicKey encodes a crypto public key to a file on disk in PEM format. +// Any file with the same name will be overwritten. +func WritePublicKey(key interface{}, out string) error { + // Remove any file with same name, if it exists. + if _, err := os.Stat(out); err == nil { + if err = os.Remove(out); err != nil { + return errors.WithStack(err) + } + } + keyOut, err := os.OpenFile(out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, + os.FileMode(0600)) + if err != nil { + return errors.Wrapf(err, + "failed to open '%s' for writing", out) + } + pubPEM, err := PublicPEM(key) + if err != nil { + return errors.Wrap(err, + "failed to convert public key to PEM block") + } + err = pem.Encode(keyOut, pubPEM) + if err != nil { + return errors.Wrapf(err, + "pem encode '%s' failed", out) + } + keyOut.Close() + return nil +} diff --git a/crypto/keys/keyPair.go b/crypto/keys/keyPair.go new file mode 100644 index 00000000..c224df00 --- /dev/null +++ b/crypto/keys/keyPair.go @@ -0,0 +1,24 @@ +package keys + +import ( + "github.com/pkg/errors" +) + +// GenerateDefaultKeyPair generates a public/private key pair using configured +// default values for key type, curve, and size. +func GenerateDefaultKeyPair() (interface{}, interface{}, error) { + return GenerateKeyPair(DefaultKeyType, DefaultKeyCurve, DefaultKeySize) +} + +// GenerateKeyPair creates an asymmetric crypto keypair using input configuration. +func GenerateKeyPair(kty, crv string, size int) (interface{}, interface{}, error) { + priv, err := GenerateKey(kty, crv, size) + if err != nil { + return nil, nil, errors.WithStack(err) + } + pub, err := PublicKey(priv) + if err != nil { + return nil, nil, errors.WithStack(err) + } + return pub, priv, err +} diff --git a/crypto/keys/key_test.go b/crypto/keys/key_test.go new file mode 100644 index 00000000..f2a6c9f4 --- /dev/null +++ b/crypto/keys/key_test.go @@ -0,0 +1,414 @@ +package keys + +import ( + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "io" + "io/ioutil" + "os" + "testing" + + "github.com/pkg/errors" + "github.com/smallstep/assert" +) + +func Test_GenerateKey_unrecognizedkt(t *testing.T) { + var failTests = []struct { + kt string + crv string + bits int + expected string + }{ + {"shake and bake", "", 2048, "unrecognized key type: shake and bake"}, + {"EC", "P-12", 0, "invalid value for argument crv (crv: 'P-12')"}, + } + + for i, tc := range failTests { + k, err := GenerateKey(tc.kt, tc.crv, tc.bits) + if assert.Error(t, err, i) { + assert.HasPrefix(t, err.Error(), tc.expected) + assert.Nil(t, k) + } + } + + var ecdsaTests = []struct { + kt string + crv string + }{ + {"EC", "P-256"}, + {"EC", "P-384"}, + {"EC", "P-521"}, + } + + for i, tc := range ecdsaTests { + k, err := GenerateKey(tc.kt, tc.crv, 0) + if assert.NoError(t, err, i) { + _, ok := k.(*ecdsa.PrivateKey) + assert.True(t, ok, i) + } + } + + k, err := GenerateKey("RSA", "", 2048) + if assert.NoError(t, err) { + _, ok := k.(*rsa.PrivateKey) + assert.True(t, ok) + } +} + +func Test_PrivatePEM(t *testing.T) { + oldX509EncryptPEMBlock := x509EncryptPEMBlock + oldX509MarshalECPrivateKey := x509MarshalECPrivateKey + + var clean = func() { + x509EncryptPEMBlock = oldX509EncryptPEMBlock + x509MarshalECPrivateKey = oldX509MarshalECPrivateKey + } + defer clean() + + tests := map[string]struct { + key func() (interface{}, error) + pass string + cipher x509.PEMCipher + setup func() + clean func() + err error + }{ + "unrecognized key type": { + key: func() (interface{}, error) { + return "shake and bake", nil + }, + pass: "pass", + cipher: x509.PEMCipherAES256, + err: errors.New("Unrecognized key - type: string, value: shake and bake"), + }, + "RSA: encrypt PEM block error": { + key: func() (interface{}, error) { + return GenerateKey("RSA", "", 1024) + }, + pass: "pass", + cipher: x509.PEMCipherAES256, + setup: func() { + x509EncryptPEMBlock = func(r io.Reader, s string, d, p []byte, alg x509.PEMCipher) (*pem.Block, error) { + return nil, errors.Errorf("force EncryptPEMBlock error") + } + }, + clean: clean, + err: errors.New("force EncryptPEMBlock error"), + }, + "EC: marshal key error": { + key: func() (interface{}, error) { + return GenerateKey("EC", "P-256", 0) + }, + pass: "pass", + cipher: x509.PEMCipherAES256, + setup: func() { + x509MarshalECPrivateKey = func(k *ecdsa.PrivateKey) ([]byte, error) { + return nil, errors.Errorf("force MarshalECPrivateKey error") + } + }, + clean: clean, + err: errors.New("Unable to marshal EC private key: force MarshalECPrivateKey error"), + }, + "EC: encrypt PEM block error": { + key: func() (interface{}, error) { + return GenerateKey("EC", "P-256", 0) + }, + pass: "pass", + cipher: x509.PEMCipherAES256, + setup: func() { + x509EncryptPEMBlock = func(r io.Reader, s string, d, p []byte, alg x509.PEMCipher) (*pem.Block, error) { + return nil, errors.Errorf("force EncryptPEMBlock error") + } + }, + clean: clean, + err: errors.New("force EncryptPEMBlock error"), + }, + "RSA: empty password generates unencrypted PEM block - success": { + key: func() (interface{}, error) { + return GenerateKey("RSA", "", 1024) + }, + }, + "EC: empty password generates unencrypted PEM block - success": { + key: func() (interface{}, error) { + return GenerateKey("EC", "P-256", 0) + }, + }, + "EC: encrypt - success": { + key: func() (interface{}, error) { + return GenerateKey("EC", "P-256", 0) + }, + pass: "pass", + cipher: x509.PEMCipherAES256, + }, + "RSA: encrypt - success": { + key: func() (interface{}, error) { + return GenerateKey("RSA", "", 256) + }, + pass: "pass", + cipher: x509.PEMCipherAES256, + }, + } + + for name, test := range tests { + t.Logf("Running test case: %s", name) + + priv, err := test.key() + assert.FatalError(t, err) + + if test.setup != nil { + test.setup() + } + p, err := PrivatePEM(priv, &EncOpts{test.pass, test.cipher}) + if test.clean != nil { + test.clean() + } + if err != nil { + if assert.NotNil(t, test.err) { + assert.HasPrefix(t, err.Error(), test.err.Error()) + } + } else { + if assert.Nil(t, test.err) { + switch k := priv.(type) { + case *rsa.PrivateKey: + if test.pass == "" { + assert.False(t, x509.IsEncryptedPEMBlock(p)) + assert.Equals(t, p.Type, "RSA PRIVATE KEY") + assert.Equals(t, p.Bytes, x509.MarshalPKCS1PrivateKey(k)) + } else { + assert.True(t, x509.IsEncryptedPEMBlock(p)) + assert.Equals(t, p.Type, "RSA PRIVATE KEY") + assert.Equals(t, p.Headers["Proc-Type"], "4,ENCRYPTED") + + der, err := x509.DecryptPEMBlock(p, []byte(test.pass)) + assert.FatalError(t, err) + assert.Equals(t, der, x509.MarshalPKCS1PrivateKey(k)) + } + case *ecdsa.PrivateKey: + if test.pass == "" { + assert.False(t, x509.IsEncryptedPEMBlock(p)) + assert.Equals(t, p.Type, "EC PRIVATE KEY") + + b, err := x509MarshalECPrivateKey(k) + assert.FatalError(t, err) + assert.Equals(t, p.Bytes, b) + } else { + assert.True(t, x509.IsEncryptedPEMBlock(p)) + assert.Equals(t, p.Type, "EC PRIVATE KEY") + assert.Equals(t, p.Headers["Proc-Type"], "4,ENCRYPTED") + + der, err := x509.DecryptPEMBlock(p, []byte(test.pass)) + assert.FatalError(t, err) + plain, err := x509.MarshalECPrivateKey(k) + assert.FatalError(t, err) + assert.Equals(t, der, plain) + } + default: + t.Errorf("Unrecognized key - type: %T, value: %v", k, k) + } + } + } + } +} + +// empty private key path throws error +func Test_LoadPrivateKey(t *testing.T) { + tests := map[string]struct { + bytes []byte + getPass func() (string, error) + err error + resultBytes []byte + }{ + "input bytes are not PEM formatted": { + bytes: nil, + err: errors.New("invalid key - key is not PEM formatted"), + }, + "getPass is nil": { + bytes: []byte(`-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,544d87909c73e30d2f43fe21b6bc6caf + +uir6r8TrhNuXTs6ZF2avaqaLX5cpuh+oTyoS+5Wk0Cbr3NQuAg1l/ZuRI8NWKBt2 +nnAXpKSOVnfut9i95ifaOf38mzdDKf8r8vAAVlT9nduzizTcc25Bst2ljSuSuOMP +eCye1+g47hWU2hbxKF1NH9LfJsS7W90LKEuSCKZljz0= +-----END EC PRIVATE KEY-----`), + getPass: nil, + err: errors.New("private key needs a decryption passphrase"), + }, + "propagate getPass error": { + bytes: []byte(`-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,544d87909c73e30d2f43fe21b6bc6caf + +uir6r8TrhNuXTs6ZF2avaqaLX5cpuh+oTyoS+5Wk0Cbr3NQuAg1l/ZuRI8NWKBt2 +nnAXpKSOVnfut9i95ifaOf38mzdDKf8r8vAAVlT9nduzizTcc25Bst2ljSuSuOMP +eCye1+g47hWU2hbxKF1NH9LfJsS7W90LKEuSCKZljz0= +-----END EC PRIVATE KEY-----`), + getPass: func() (string, error) { + return "", errors.Errorf("force getPass error") + }, + err: errors.New("force getPass error"), + }, + "propagate decryptPEMBlock error": { + bytes: []byte(`-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,544d87909c73e30d2f43fe21b6bc6caf + +uir6r8TrhNuXTs6ZF2avaqaLX5cpuh+oTyoS+5Wk0Cbr3NQuAg1l/ZuRI8NWKBt2 +nnAXpKSOVnfut9i95ifaOf38mzdDKf8r8vAAVlT9nduzizTcc25Bst2ljSuSuOMP +eCye1+g47hWU2hbxKF1NH9LfJsS7W90LKEuSCKZljz0= +-----END EC PRIVATE KEY-----`), + getPass: func() (string, error) { + return "ricky-bobby", nil + }, + err: errors.New("x509: decryption password incorrect"), + }, + "EC: encrypted success": { + bytes: []byte(`-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,544d87909c73e30d2f43fe21b6bc6caf + +uir6r8TrhNuXTs6ZF2avaqaLX5cpuh+oTyoS+5Wk0Cbr3NQuAg1l/ZuRI8NWKBt2 +nnAXpKSOVnfut9i95ifaOf38mzdDKf8r8vAAVlT9nduzizTcc25Bst2ljSuSuOMP +eCye1+g47hWU2hbxKF1NH9LfJsS7W90LKEuSCKZljz0= +-----END EC PRIVATE KEY-----`), + resultBytes: []byte{48, 119, 2, 1, 1, 4, 32, 143, 215, 97, 167, 20, 68, 24, 34, 8, 221, 52, 8, 69, 10, 212, 144, 108, 53, 76, 164, 150, 247, 133, 247, 71, 39, 38, 148, 250, 36, 124, 179, 160, 10, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 161, 68, 3, 66, 0, 4, 181, 104, 199, 201, 196, 120, 38, 193, 82, 208, 46, 198, 19, 153, 195, 113, 207, 143, 99, 212, 191, 242, 188, 32, 78, 244, 154, 187, 49, 128, 223, 46, 11, 21, 137, 216, 237, 138, 98, 170, 118, 224, 239, 136, 220, 61, 82, 84, 223, 146, 99, 150, 190, 165, 18, 63, 69, 21, 10, 52, 48, 16, 128, 173}, + getPass: func() (string, error) { + return "pass", nil + }, + }, + "EC: un-encrypted success": { + bytes: []byte(`-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIJhTbozTaR/CoOj7yZIhyteXaYXxW/4RF6D/pMA2y4XeoAoGCCqGSM49 +AwEHoUQDQgAERpymPrkl64FD4vPlljJaoc5rMhTdWZsk/G1H/X7mDtlDhoHmrRp5 +aJ6EbWtKrZ0+Eb82rZW207IzoTJFnpFPdA== +-----END EC PRIVATE KEY-----`), + resultBytes: []byte{48, 119, 2, 1, 1, 4, 32, 152, 83, 110, 140, 211, 105, 31, 194, 160, 232, 251, 201, 146, 33, 202, 215, 151, 105, 133, 241, 91, 254, 17, 23, 160, 255, 164, 192, 54, 203, 133, 222, 160, 10, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 161, 68, 3, 66, 0, 4, 70, 156, 166, 62, 185, 37, 235, 129, 67, 226, 243, 229, 150, 50, 90, 161, 206, 107, 50, 20, 221, 89, 155, 36, 252, 109, 71, 253, 126, 230, 14, 217, 67, 134, 129, 230, 173, 26, 121, 104, 158, 132, 109, 107, 74, 173, 157, 62, 17, 191, 54, 173, 149, 182, 211, 178, 51, 161, 50, 69, 158, 145, 79, 116}, + }, + "RSA: encrypted success": { + bytes: []byte(`-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,dfc9d24f6f09f401a4bf297098e414ed + +4z6W9hmXmJNj1gNhpzwZy2GWWmzFUrzRmUHZmpJkpW1zObaDWyhlC0dhoexPUHyM +elVwEtIfk4hHW/KUHru5yx83ce8Y29I/FTvYlLjK4R/fF4hskrWyNKOy4WRFeSFy ++h8SqjM+KnTpRrhpZarGaGc32RCSCE1OK0KtFVKXNYYyTQLuD2sEPCR0hj9SkgAq +KsYKSvCl9KJR27iilvhXQ7UvuE32OReth6uJfcyF4uY= +-----END RSA PRIVATE KEY-----`), + resultBytes: []byte{48, 129, 170, 2, 1, 0, 2, 33, 0, 182, 202, 226, 207, 7, 173, 171, 119, 158, 212, 232, 208, 16, 216, 31, 13, 248, 121, 11, 253, 97, 212, 247, 249, 39, 165, 50, 207, 197, 128, 154, 41, 2, 3, 1, 0, 1, 2, 32, 123, 241, 3, 106, 231, 68, 237, 175, 181, 69, 157, 250, 126, 129, 92, 68, 1, 100, 255, 251, 15, 212, 34, 174, 1, 128, 65, 119, 19, 78, 225, 1, 2, 17, 0, 198, 241, 220, 208, 114, 45, 98, 128, 135, 231, 202, 212, 92, 40, 29, 161, 2, 17, 0, 235, 55, 40, 19, 226, 0, 0, 15, 75, 147, 144, 130, 48, 202, 95, 137, 2, 17, 0, 153, 133, 24, 157, 238, 13, 225, 190, 71, 161, 242, 30, 47, 195, 113, 33, 2, 16, 41, 254, 214, 11, 254, 188, 203, 69, 239, 211, 111, 232, 158, 183, 115, 41, 2, 16, 102, 42, 237, 52, 125, 228, 216, 129, 192, 79, 251, 183, 46, 44, 230, 140}, + getPass: func() (string, error) { + return "pass", nil + }, + }, + "RSA: un-encrypted success": { + bytes: []byte(`-----BEGIN RSA PRIVATE KEY----- +MIGsAgEAAiEA1Yz+DRsCJlrxp+Rka52JIPkaCCIbyj4+EUHao/dyGvMCAwEAAQIg +BUxcOUMESKNU/49hFnJwJn+tfLwOQzu7ozdGe8aclVECEQDq33SrABt4+DOGPTJS +1Cx9AhEA6MKKZ9Y0mLy+Gc/5gAiwLwIRAJTGcNd0nPJWfgS1NPBEl90CEQC3Sjrj +efMBM+AfQ38eK7lRAhEA32j/3N2dXwOlytURq2yl+Q== +-----END RSA PRIVATE KEY-----`), + resultBytes: []byte{48, 129, 172, 2, 1, 0, 2, 33, 0, 213, 140, 254, 13, 27, 2, 38, 90, 241, 167, 228, 100, 107, 157, 137, 32, 249, 26, 8, 34, 27, 202, 62, 62, 17, 65, 218, 163, 247, 114, 26, 243, 2, 3, 1, 0, 1, 2, 32, 5, 76, 92, 57, 67, 4, 72, 163, 84, 255, 143, 97, 22, 114, 112, 38, 127, 173, 124, 188, 14, 67, 59, 187, 163, 55, 70, 123, 198, 156, 149, 81, 2, 17, 0, 234, 223, 116, 171, 0, 27, 120, 248, 51, 134, 61, 50, 82, 212, 44, 125, 2, 17, 0, 232, 194, 138, 103, 214, 52, 152, 188, 190, 25, 207, 249, 128, 8, 176, 47, 2, 17, 0, 148, 198, 112, 215, 116, 156, 242, 86, 126, 4, 181, 52, 240, 68, 151, 221, 2, 17, 0, 183, 74, 58, 227, 121, 243, 1, 51, 224, 31, 67, 127, 30, 43, 185, 81, 2, 17, 0, 223, 104, 255, 220, 221, 157, 95, 3, 165, 202, 213, 17, 171, 108, 165, 249}, + }, + } + for name, test := range tests { + t.Logf("Running test case: %s", name) + + key, err := LoadPrivateKey(test.bytes, test.getPass) + if err != nil { + if assert.NotNil(t, test.err) { + assert.HasPrefix(t, err.Error(), test.err.Error()) + } + } else { + if assert.Nil(t, test.err) { + switch k := key.(type) { + case *ecdsa.PrivateKey: + b, err := x509.MarshalECPrivateKey(k) + assert.FatalError(t, err) + assert.Equals(t, b, test.resultBytes) + case *rsa.PrivateKey: + assert.Equals(t, x509.MarshalPKCS1PrivateKey(k), test.resultBytes) + default: + t.Errorf("unrecognized key type %T", k) + } + } + } + } +} + +func Test_WriteKey(t *testing.T) { + keyOut := "./test.key" + pass := "pass" + + tests := map[string]struct { + key func() (interface{}, error) + keyOut string + pass string + err error + }{ + "propagate open key out file error": { + key: func() (interface{}, error) { + return GenerateKey(DefaultKeyType, DefaultKeyCurve, 0) + }, + keyOut: "./fakeDir/test.key", + err: errors.New("failed to open './fakeDir/test.key' for writing: open ./fakeDir/test.key: no such file or directory"), + }, + "propagate encrypt key error": { + key: func() (interface{}, error) { + return GenerateKey(DefaultKeyType, DefaultKeyCurve, 0) + }, + pass: pass, + keyOut: keyOut, + err: errors.New("failed to convert private key to PEM block: encryption passphrase cannot be empty"), + }, + "success": { + key: func() (interface{}, error) { + return GenerateKey(DefaultKeyType, DefaultKeyCurve, 0) + }, + keyOut: keyOut, + pass: pass, + err: errors.New("failed to convert private key to PEM block: encryption passphrase cannot be empty"), + }, + } + + for name, test := range tests { + t.Logf("Running test case: %s", name) + + key, err := test.key() + assert.FatalError(t, err) + + err = WritePrivateKey(key, test.pass, test.keyOut) + if err != nil { + if assert.NotNil(t, test.err) { + assert.HasPrefix(t, err.Error(), test.err.Error()) + } + } else { + // Check key permissions + fileInfo, err := os.Stat(test.keyOut) + assert.FatalError(t, err) + fileMode := fileInfo.Mode() + if fileMode != 0600 { + t.Errorf("FileMode mismatch for file %s -- expected: `%d`, but got: `%d`", + test.keyOut, fileMode, 0600) + } + + switch k := key.(type) { + case *ecdsa.PrivateKey: + // Verify that key written to file is correct + plain, err := x509.MarshalECPrivateKey(k) + assert.FatalError(t, err) + keyFileBytes, err := ioutil.ReadFile(test.keyOut) + assert.FatalError(t, err) + pemKey, _ := pem.Decode(keyFileBytes) + assert.True(t, x509.IsEncryptedPEMBlock(pemKey)) + assert.Equals(t, pemKey.Type, "EC PRIVATE KEY") + assert.Equals(t, pemKey.Headers["Proc-Type"], "4,ENCRYPTED") + der, err := x509.DecryptPEMBlock(pemKey, []byte(pass)) + assert.FatalError(t, err) + assert.Equals(t, der, plain) + default: + t.Errorf("unexpected key type %T", k) + } + } + } +} diff --git a/crypto/random.go b/crypto/random.go new file mode 100644 index 00000000..1076acd5 --- /dev/null +++ b/crypto/random.go @@ -0,0 +1,46 @@ +package crypto + +import ( + "crypto/rand" + "math/big" +) + +// GenerateRandomASCIIString returns a securely generated random ASCII string. +// It reads random numbers from crypto/rand and searches for printable characters. +// It will return an error if the system's secure random number generator fails to +// function correctly, in which case the caller must not continue. +func GenerateRandomASCIIString(length int) (string, error) { + result := "" + for { + if len(result) >= length { + return result, nil + } + num, err := rand.Int(rand.Reader, big.NewInt(int64(127))) + if err != nil { + return "", err + } + n := num.Int64() + // Make sure that the number/byte/letter is inside + // the range of printable ASCII characters (excluding space and DEL) + if n > 32 && n < 127 { + result += string(n) + } + } +} + +// GenerateRandomRestrictedString returns a securely generated random ASCII string. +// It reads random numbers from crypto/rand and searches for printable characters. +// It will return an error if the system's secure random number generator fails to +// function correctly, in which case the caller must not continue. +func GenerateRandomRestrictedString(length int) (string, error) { + const chars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + result := make([]byte, length) + for i := range result { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars)))) + if err != nil { + return "", err + } + result[i] = chars[num.Int64()] + } + return string(result), nil +} diff --git a/crypto/test_utils/key/badca_rsa_key b/crypto/test_utils/key/badca_rsa_key new file mode 100644 index 00000000..74aae1d2 --- /dev/null +++ b/crypto/test_utils/key/badca_rsa_key @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,6D4455D3019FC904 + +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 +-----END RSA PRIVATE KEY----- diff --git a/crypto/test_utils/key/badpem_rsa_key b/crypto/test_utils/key/badpem_rsa_key new file mode 100644 index 00000000..a31402a3 --- /dev/null +++ b/crypto/test_utils/key/badpem_rsa_key @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,6D4455D3019FC904 + +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 +-----END RSA PRIVATE KEY----- diff --git a/crypto/test_utils/key/ca_ecdsa_key b/crypto/test_utils/key/ca_ecdsa_key new file mode 100644 index 00000000..ae532319 --- /dev/null +++ b/crypto/test_utils/key/ca_ecdsa_key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,15FAB0B97397621DF6DE7E2DAAAC0313 + +BOicR28Sb+aRZIOjvRc6KSH08teBRFPfKnMy0nqbcom/fLqAjA/2rs5SQ3tjO4UR +YpddJsO3g9rnlyi28xY1nDqtjs41zRK5LmhJP75V5We4G+Bm0f+mHQTF6BOmS4Dv +CwqNz77I95A/LAPimyQV1PJW0z52Yw/NzXYMWJG4vEs= +-----END EC PRIVATE KEY----- diff --git a/crypto/test_utils/key/ca_rsa_key b/crypto/test_utils/key/ca_rsa_key new file mode 100644 index 00000000..85e782cb --- /dev/null +++ b/crypto/test_utils/key/ca_rsa_key @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,36945dfbb2645820d163fad834a3d290 + +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 +-----END RSA PRIVATE KEY----- diff --git a/crypto/test_utils/key/noPasscodeBadCa_rsa_key b/crypto/test_utils/key/noPasscodeBadCa_rsa_key new file mode 100644 index 00000000..90caebe6 --- /dev/null +++ b/crypto/test_utils/key/noPasscodeBadCa_rsa_key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/crypto/test_utils/key/noPasscodeCA_ecdsa_key b/crypto/test_utils/key/noPasscodeCA_ecdsa_key new file mode 100644 index 00000000..95241d02 --- /dev/null +++ b/crypto/test_utils/key/noPasscodeCA_ecdsa_key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIEZVK8QMUOQK09NkTsoGXCBeniAERbR2eow5OB66qQqeoAoGCCqGSM49 +AwEHoUQDQgAETbf9/jeZxYmhPeLsfCP77x7vvfLw1NOd+eMzJTdIqUletHs3LSHU +0D3b/fMlRJq498hFf7brnwQXfJiW9+gNng== +-----END EC PRIVATE KEY----- diff --git a/crypto/test_utils/key/noPasscodeCa_rsa_key b/crypto/test_utils/key/noPasscodeCa_rsa_key new file mode 100644 index 00000000..056875b5 --- /dev/null +++ b/crypto/test_utils/key/noPasscodeCa_rsa_key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/errs/errs.go b/errs/errs.go new file mode 100644 index 00000000..5bf28475 --- /dev/null +++ b/errs/errs.go @@ -0,0 +1,256 @@ +package errs + +import ( + "fmt" + "os" + "strings" + + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +// err errExitCode is the default exit code when an error occurs. +const errExitCode = 1 + +// ErrTooFewArgs occurs when too few arguments were provided by the user +var ErrTooFewArgs = NewError("Not enough arguments were provided") + +// ErrTooManyArgs occurs when too many arguments were provided by the user +var ErrTooManyArgs = NewError("Too many arguments were provided") + +// ErrMissingArgs occurs when one or more arguments are missing +var ErrMissingArgs = NewError("An incorrect number of arguments were provided") + +// ErrMissingToken occurs when a STEP_TOKEN or --token flag is not provided +var ErrMissingToken = NewError("A one-time token must be provided to bootstrap the identity via the `--token` flag or `$STEP_TOKEN` environment variable") + +// ErrMissingCAURL occurs when a STEP_CA_URL or --ca-url flag is not provided +var ErrMissingCAURL = NewError("The CA URL must be provided through the --ca-url flag or `$STEP_CA_URL` environment variable") + +// NewError returns a new Error for the given format and arguments +func NewError(format string, args ...interface{}) error { + return errors.Errorf(format, args...) +} + +// NewExitError returns an error than the urfave/cli package will handle and +// will show the given error and exit with the given code. +func NewExitError(err error, exitCode int) error { + return cli.NewExitError(err, exitCode) +} + +// Wrap returns a new error wrapped by the given error with the given message. +// If the given error implements the errors.Cause interface, the base error is +// used. If the given error is wrapped by a package name, the error wrapped +// will be the string after the last colon. +func Wrap(err error, format string, args ...interface{}) error { + cause := errors.Cause(err) + if cause == err { + str := err.Error() + if i := strings.LastIndexByte(str, ':'); i >= 0 { + str = strings.TrimSpace(str[i:]) + return errors.Wrapf(fmt.Errorf(str), format, args...) + } + } + return errors.Wrapf(cause, format, args...) +} + +// UsageExitError prints out the usage error followed by the help documentation +// for the command +func UsageExitError(c *cli.Context, err error) error { + msg := fmt.Sprintf("Error: %s\n\n%s", err.Error(), usageString(c)) + return cli.NewExitError(msg, errExitCode) +} + +// UnexpectedExitError wraps the error denoting that it was unexpected +func UnexpectedExitError(err error) error { + msg := fmt.Sprintf("Error: An unexpected error was encountered: %s", err.Error()) + return cli.NewExitError(msg, errExitCode) +} + +// ToError transforms the given error into our frameworks error type +func ToError(err error) error { + switch err.(type) { + case nil: + return nil + default: + return cli.NewExitError(prependErrorMsg(err), errExitCode) + } +} + +func prependErrorMsg(err error) string { + m := err.Error() + if strings.HasPrefix(m, "Error:") { + return m + } + + return "Error: " + m +} + +// InsecureCommand returns an error with a message saying that the current +// command requires the insecure flag. +func InsecureCommand(ctx *cli.Context) error { + return errors.Errorf("'%s %s' requires the '--insecure' flag", ctx.App.Name, ctx.Command.Name) +} + +// EqualArguments returns an error saying that the given positional arguments +// cannot be equal. +func EqualArguments(ctx *cli.Context, arg1, arg2 string) error { + return errors.Errorf("positional arguments <%s> and <%s> cannot be equal in '%s'", arg1, arg2, usage(ctx)) +} + +// MissingArguments returns an error with a missing arguments message for the +// given positional argument names. +func MissingArguments(ctx *cli.Context, argNames ...string) error { + switch len(argNames) { + case 0: + return errors.Errorf("missing positional arguments in '%s'", usage(ctx)) + case 1: + return errors.Errorf("missing positional argument <%s> in '%s'", argNames[0], usage(ctx)) + default: + args := make([]string, len(argNames)) + for i, name := range argNames { + args[i] = "<" + name + ">" + } + return errors.Errorf("missing positional argument %s in '%s'", strings.Join(args, " "), usage(ctx)) + } +} + +// NumberOfArguments returns nil if the number of positional arguments is +// equal to the required one. It will return an appropriate error if they are +// not. +func NumberOfArguments(ctx *cli.Context, required int) error { + n := ctx.NArg() + switch { + case n < required: + return TooFewArguments(ctx) + case n > required: + return TooManyArguments(ctx) + default: + return nil + } +} + +// TooFewArguments returns an error with a few arguments were provided message. +func TooFewArguments(ctx *cli.Context) error { + return errors.Errorf("not enough positional arguments were provided in '%s'", usage(ctx)) +} + +// TooManyArguments returns an error with a too many arguments were provided +// message. +func TooManyArguments(ctx *cli.Context) error { + return errors.Errorf("too many positional arguments were provided in '%s'", usage(ctx)) +} + +// InsecureArgument returns an error with the given argument requiring the +// --insecure flag. +func InsecureArgument(ctx *cli.Context, name string) error { + return errors.Errorf("positional argument <%s> requires the '--insecure' flag", name) +} + +// FlagValueInsecure returns an error with the given flag and value requiring +// the --insecure flag. +func FlagValueInsecure(ctx *cli.Context, flag string, value string) error { + return errors.Errorf("flag '--%s %s' requires the '--insecure' flag", flag, value) +} + +// InvalidFlagValue returns an error with the given value being missing or +// invalid for the given flag. Optionally it lists the given formated options +// at the end. +func InvalidFlagValue(ctx *cli.Context, flag string, value string, options string) error { + var format string + if len(value) == 0 { + format = fmt.Sprintf("missing value for flag '--%s'", flag) + } else { + format = fmt.Sprintf("invalid value '%s' for flag '--%s'", value, flag) + } + + if len(options) == 0 { + return errors.New(format) + } + + return errors.New(format + " options are " + options) +} + +// IncompatibleFlag returns an error with the flag being incompatible with the +// given value. +func IncompatibleFlag(ctx *cli.Context, flag string, value string) error { + return errors.Errorf("flag '--%s' is incompatible with '%s'", flag, value) +} + +// RequiredFlag returns an error with the required flag message. +func RequiredFlag(ctx *cli.Context, flag string) error { + return errors.Errorf("'%s %s' requires the '--%s' flag", ctx.App.HelpName, + ctx.Command.Name, flag) +} + +// RequiredWithFlag returns an error with the required flag message with another flag. +func RequiredWithFlag(ctx *cli.Context, required, with string) error { + return errors.Errorf("flag '--%s' requires the '--%s' flag", required, with) +} + +// RequiredInsecureFlag returns an error with the required flag message unless +// the insecure flag is used. +func RequiredInsecureFlag(ctx *cli.Context, flag string) error { + return errors.Errorf("flag '--%s' requires the '--insecure' flag", flag) +} + +// RequiredSubtleFlag returns an error with the required flag message unless +// the subtle flag is used. +func RequiredSubtleFlag(ctx *cli.Context, flag string) error { + return errors.Errorf("flag '--%s' requires the --subtle' flag", flag) +} + +// RequiredOrFlag returns an error with a list of flags being required messages. +func RequiredOrFlag(ctx *cli.Context, flags ...string) error { + params := make([]string, len(flags)) + for i, flag := range flags { + params[i] = "--" + flag + } + return errors.Errorf("flag %s are required", strings.Join(params, " or ")) +} + +// MinSizeFlag returns an error with a greater or equal message message for +// the given flag and size. +func MinSizeFlag(ctx *cli.Context, flag string, size string) error { + return errors.Errorf("flag '--%s' must be greater or equal than %s", flag, size) +} + +// MinSizeInsecureFlag returns an error with a requiring --insecure flag +// message with the given flag an size. +func MinSizeInsecureFlag(ctx *cli.Context, flag, size string) error { + return errors.Errorf("flag '--%s' requires at least %s unless '--insecure' flag is provided", flag, size) +} + +// MutuallyExclusiveFlags returns an error with mutually exclusive message for +// the given flags. +func MutuallyExclusiveFlags(ctx *cli.Context, flag1, flag2 string) error { + return errors.Errorf("flag '--%s' and flag '--%s' are mutually exclusive", flag1, flag2) +} + +// usage returns the command usage text if set or a default usage string. +func usage(ctx *cli.Context) string { + if len(ctx.Command.UsageText) == 0 { + return fmt.Sprintf("%s %s [command options]", ctx.App.HelpName, ctx.Command.Name) + } + + return ctx.Command.UsageText +} + +// usageString returns the command usage prepended by the string "Usage: ". +func usageString(ctx *cli.Context) string { + return "Usage: " + usage(ctx) +} + +// FileError is a wrapper for errors of the os package. +func FileError(err error, filename string) error { + switch e := errors.Cause(err).(type) { + case *os.PathError: + return errors.Errorf("%s %s failed: %v", e.Op, e.Path, e.Err) + case *os.LinkError: + return errors.Errorf("%s %s %s failed %v:", e.Op, e.Old, e.New, e.Err) + case *os.SyscallError: + return errors.Errorf("%s failed %v:", e.Syscall, e.Err) + default: + return Wrap(err, "unexpected error on %s", filename) + } +} diff --git a/exec/exec.go b/exec/exec.go new file mode 100644 index 00000000..87265ce7 --- /dev/null +++ b/exec/exec.go @@ -0,0 +1,145 @@ +package exec + +import ( + "fmt" + "os" + "os/exec" + "os/signal" + "path" + "runtime" + "strconv" + "syscall" + + "github.com/pkg/errors" +) + +// Exec is wrapper over syscall.Exec, invokes the execve(2) system call. On +// windows it executes Run with the same arguments. +func Exec(name string, arg ...string) { + if runtime.GOOS == "windows" { + Run(name, arg...) + return + } + args := append([]string{name}, arg...) + if err := syscall.Exec(name, args, os.Environ()); err != nil { + errorAndExit(name, err) + } +} + +// Run is a wrapper over os/exec Cmd.Run that configures Stderr/Stdin/Stdout +// to the current ones and wait until the process finishes, exiting with the +// same code. Run will also forward all the signals sent to step to the +// command. +func Run(name string, arg ...string) { + cmd, exitCh, err := run(name, arg...) + if err != nil { + errorAndExit(name, err) + } + + if err = cmd.Wait(); err != nil { + errorf(name, err) + } + + // exit and wait until os.Exit + exitCh <- getExitStatus(cmd) + exitCh <- 0 +} + +// RunWithPid calls Run and writes the process ID in pidFile. +func RunWithPid(pidFile, name string, arg ...string) { + f, err := os.OpenFile(pidFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + errorAndExit(name, err) + } + + // Run process + cmd, exitCh, err := run(name, arg...) + if err != nil { + f.Close() + os.Remove(f.Name()) + errorAndExit(name, err) + } + + // Write pid + f.Write([]byte(strconv.Itoa(cmd.Process.Pid))) + f.Close() + + // Wait until it finishes + if err = cmd.Wait(); err != nil { + errorf(name, err) + } + + // clean, exit and wait until os.Exit + os.Remove(f.Name()) + exitCh <- getExitStatus(cmd) + exitCh <- 0 +} + +// OpenInBrowser opens the given url on a web browser +func OpenInBrowser(url string) error { + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", url) + case "linux": + cmd = exec.Command("xdg-open", url) + case "windows": + cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) + default: + return errors.Errorf("unsupported platform '%s'", runtime.GOOS) + } + + return errors.WithStack(cmd.Start()) +} + +func run(name string, arg ...string) (*exec.Cmd, chan int, error) { + cmd := exec.Command(name, arg...) + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + + // Start process + if err := cmd.Start(); err != nil { + return nil, nil, err + } + + // Forward signals + exitCh := make(chan int) + go signalHandler(cmd, exitCh) + + return cmd, exitCh, nil +} + +func getExitStatus(cmd *exec.Cmd) int { + if cmd.ProcessState != nil { + switch sys := cmd.ProcessState.Sys().(type) { + case syscall.WaitStatus: + return sys.ExitStatus() + } + } + return 1 +} + +func errorf(name string, err error) { + fmt.Fprintf(os.Stderr, "%s: %s\n", path.Base(name), err.Error()) +} + +func errorAndExit(name string, err error) { + fmt.Fprintf(os.Stderr, "%s: %s\n", path.Base(name), err.Error()) + os.Exit(-1) +} + +// signalHandler forwards all the signals to the cmd. +func signalHandler(cmd *exec.Cmd, exitCh chan int) { + signals := make(chan os.Signal) + signal.Notify(signals) + defer signal.Stop(signals) + for { + select { + case sig := <-signals: + cmd.Process.Signal(sig) + case code := <-exitCh: + os.Exit(code) + } + } +} diff --git a/flags/flags.go b/flags/flags.go new file mode 100644 index 00000000..fa8c1351 --- /dev/null +++ b/flags/flags.go @@ -0,0 +1,206 @@ +package flags + +import ( + "fmt" + "strings" + + "github.com/urfave/cli" +) + +// OldPasswordFile returns a flag for receiving an old password +func OldPasswordFile(usage string) cli.Flag { + if usage == "" { + usage = "The path to the `FILE` containing the old encryption password" + } + + return cli.StringFlag{ + Name: "old-password-file, o", + Usage: usage, + EnvVar: "STEP_OLD_PASSWORD_FILE", + } +} + +// NewPasswordFile returns a flag for receiving a new password +func NewPasswordFile(usage string) cli.Flag { + if usage == "" { + usage = "The path to the `FILE` containing the new encryption password" + } + + return cli.StringFlag{ + Name: "new-password-file, n", + Usage: usage, + EnvVar: "STEP_NEW_PASSWORD_FILE", + } +} + +// Bits returns a flag for receiving the number of bits in generating a key +func Bits(usage string, value int) cli.Flag { + if usage == "" { + usage = "Number of bits used to generate the private key" + } + + if value == 0 { + value = 256 + } + + return cli.IntFlag{ + Name: "bits, b", + Usage: usage, + EnvVar: "STEP_BITS", + Value: value, + } +} + +// Action returns a flag for receiving an action out of several possibilities +func Action(usage string, possibilities []string, value string) cli.Flag { + usage = fmt.Sprintf("%s (Options: %s)", usage, strings.Join(possibilities, ", ")) + return cli.StringFlag{ + Name: "action, a", + Usage: usage, + EnvVar: "STEP_ACTION", + Value: value, + } +} + +// Type returns a flag for receiving a type of thing to create out of several +// possibilties +func Type(usage string, possibilities []string, value string) cli.Flag { + usage = fmt.Sprintf("%s (Options: %s)", usage, strings.Join(possibilities, ", ")) + return cli.StringFlag{ + Name: "type, t", + Usage: usage, + EnvVar: "STEP_TYPE", + Value: value, + } +} + +// Alg returns a flag for receiving the type of algorithm to use when performing an operation +func Alg(usage string, possibilities []string, value string) cli.Flag { + usage = fmt.Sprintf("%s (Options: %s)", usage, strings.Join(possibilities, ", ")) + return cli.StringFlag{ + Name: "alg", + Usage: usage, + EnvVar: "STEP_ALG", + Value: value, + } +} + +// RootCertificate returns a flag for specifying the path to a root certificate +func RootCertificate(usage string) cli.Flag { + if usage == "" { + usage = "The file `PATH` to the root certificate" + } + + return cli.StringFlag{ + Name: "root, r", + Usage: usage, + EnvVar: "STEP_ROOT_CERTIFICATE", + } +} + +// PasswordFile returns a flag for specifying the path to a file containing a password +func PasswordFile(usage string) cli.Flag { + if usage == "" { + usage = "Path to file containing a password" + } + + return cli.StringFlag{ + Name: "password-file, p", + Usage: usage, + EnvVar: "STEP_PASSWORD_FILE", + } +} + +// OutputFile returns a flag for specifying the path inwhich to write output too +func OutputFile(usage string) cli.Flag { + if usage == "" { + usage = "Path to where the output should be written" + } + + return cli.StringFlag{ + Name: "output-file, o", + Usage: usage, + EnvVar: "STEP_OUTPUT_FILE", + } +} + +// Number returns a flag for collecting the number of something to create +func Number(usage string) cli.Flag { + if usage == "" { + usage = "The `NUMBER` of entities to create" + } + + return cli.StringFlag{ + Name: "number, n", + Usage: usage, + EnvVar: "STEP_NUMBER", + } +} + +// Prefix returns a flag for prefixing to the name of an entity during creation +func Prefix(usage, value string) cli.Flag { + if usage == "" { + usage = "The `PREFIX` to apply to the names of all created entities" + } + + return cli.StringFlag{ + Name: "prefix, p", + Usage: usage, + Value: value, + EnvVar: "STEP_PREFIX", + } +} + +// OAuthProvider returns a flag for allowing the user to select an oauth provider +func OAuthProvider(usage string, providers []string, value string) cli.Flag { + usage = fmt.Sprintf("%s (Options: %s)", usage, strings.Join(providers, ", ")) + return cli.StringFlag{ + Name: "provider, idp", + Usage: usage, + Value: value, + EnvVar: "STEP_PROVIDER", + } +} + +// Email returns a flag allowing the user to specify their email +func Email(usage string) cli.Flag { + if usage == "" { + usage = "Email to use" + } + + return cli.StringFlag{ + Name: "email, e", + Usage: usage, + EnvVar: "STEP_EMAIL", + } +} + +// Console returns a flag allowing the user to specify whether or not they want +// to remain entirely in the console +func Console(usage string) cli.Flag { + if usage == "" { + usage = "Whether or not to remain entirely in the console to complete the action" + } + + return cli.BoolFlag{ + Name: "console, c", + Usage: usage, + EnvVar: "STEP_CONSOLE", + } +} + +// Limit returns a flag for limiting the results return by a command +func Limit(usage string, value int) cli.Flag { + if usage == "" { + usage = "The maximum `NUMBER` of results to return" + } + if value == 0 { + value = 10 + } + + return cli.IntFlag{ + Name: "limit, l", + Usage: usage, + Value: value, + } +} diff --git a/integration/command.go b/integration/command.go new file mode 100644 index 00000000..4943351c --- /dev/null +++ b/integration/command.go @@ -0,0 +1,133 @@ +package integration + +import ( + "bytes" + "fmt" + "io" + "os/exec" + "regexp" + "strings" + "testing" + + "github.com/smallstep/assert" +) + +// Command executes a shell command. +func Command(command string) *exec.Cmd { + return exec.Command("sh", "-c", command) +} + +// ExitError converts an error to an exec.ExitError. +func ExitError(err error) (*exec.ExitError, bool) { + v, ok := err.(*exec.ExitError) + return v, ok +} + +// Output executes a shell command and returns output from stdout. +func Output(command string) ([]byte, error) { + return Command(command).Output() +} + +// CombinedOutput executes a shell command and returns combined output from +// stdout and stderr. +func CombinedOutput(command string) ([]byte, error) { + return Command(command).CombinedOutput() +} + +// WithStdin executes a shell command with a provided reader used for stdin. +func WithStdin(command string, r io.Reader) ([]byte, error) { + cmd := Command(command) + cmd.Stdin = r + return cmd.Output() +} + +// CLICommand repreents a command-line command to execute. +type CLICommand struct { + command string + arguments string + flags map[string]string + stdin io.Reader +} + +// CLIOutput represents the output from executing a CLICommand. +type CLIOutput struct { + stdout string + stderr string + combined string +} + +// NewCLICommand generates a new CLICommand. +func NewCLICommand() CLICommand { + return CLICommand{"", "", make(map[string]string), nil} +} + +func (c CLICommand) setFlag(flag, value string) CLICommand { + flags := make(map[string]string) + for k, v := range c.flags { + flags[k] = v + } + flags[flag] = value + return CLICommand{c.command, c.arguments, flags, c.stdin} +} + +func (c CLICommand) setCommand(command string) CLICommand { + return CLICommand{command, c.arguments, c.flags, c.stdin} +} + +func (c CLICommand) setArguments(arguments string) CLICommand { + return CLICommand{c.command, arguments, c.flags, c.stdin} +} + +func (c CLICommand) setStdin(stdin string) CLICommand { + return CLICommand{c.command, c.arguments, c.flags, strings.NewReader(stdin)} +} + +func (c CLICommand) cmd() string { + flags := "" + for key, value := range c.flags { + if strings.Contains(value, " ") { + value = "\"" + value + "\"" + } + flags += fmt.Sprintf("--%s %s ", key, value) + } + return fmt.Sprintf("%s %s %s", c.command, c.arguments, flags) +} + +func (c CLICommand) run() (CLIOutput, error) { + var stdout, stderr, combined bytes.Buffer + cmd := Command(c.cmd()) + cmd.Stdout = io.MultiWriter(&stdout, &combined) + cmd.Stderr = io.MultiWriter(&stderr, &combined) + cmd.Stdin = c.stdin + err := cmd.Run() + return CLIOutput{string(stdout.Bytes()), string(stderr.Bytes()), string(combined.Bytes())}, err +} + +func (c CLICommand) test(t *testing.T, name string, expected string, msg ...interface{}) { + t.Run(name, func(t *testing.T) { + out, err := c.run() + assert.FatalError(t, err, fmt.Sprintf("`%s`: returned error '%s'\n\nOutput:\n%s", c.cmd(), err, out.combined)) + assert.Equals(t, out.combined, expected, msg) + }) +} + +func (c CLICommand) fail(t *testing.T, name string, expected interface{}, msg ...interface{}) { + t.Run(name, func(t *testing.T) { + out, err := c.run() + if assert.NotNil(t, err) { + assert.Equals(t, err.Error(), "exit status 1") + } + switch v := expected.(type) { + case string: + assert.Equals(t, expected, out.stderr) + case *regexp.Regexp: + re := expected.(*regexp.Regexp) + if !re.MatchString(out.stderr) { + t.Errorf("Error message did not match regex:\n Regex: %s\n\n Output:\n%s", re.String(), out.stderr) + } + default: + t.Errorf("unexpected type %T", v) + } + assert.Equals(t, "", out.stdout) + }) +} diff --git a/integration/integration_test.go b/integration/integration_test.go new file mode 100644 index 00000000..6632e5a2 --- /dev/null +++ b/integration/integration_test.go @@ -0,0 +1,54 @@ +// +build integration + +package integration + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "os" + "strings" + "testing" + "time" + + "github.com/smallstep/assert" +) + +const ( + TempDirectory = "testdata-tmp" +) + +func TestMain(m *testing.M) { + flag.Parse() + os.Setenv("PATH", os.Getenv("GOPATH")+"/src/github.com/smallstep/cli/bin"+":"+os.Getenv("PATH")) + if err := os.Mkdir(TempDirectory, os.ModeDir|os.ModePerm); err != nil { + log.Fatal(err) + } + rval := m.Run() + if err := os.RemoveAll(TempDirectory); err != nil { + log.Fatal(err) + } + os.Exit(rval) +} + +func TestVersion(t *testing.T) { + out, err := Output("step version | head -1") + assert.FatalError(t, err) + assert.True(t, strings.HasPrefix(string(out), "Smallstep CLI")) +} + +func TestCryptoJWTSign(t *testing.T) { + out, err := Output("step crypto jwt sign -key testdata/p256.pem -iss TestIssuer -aud TestAudience -sub TestSubject -nbf 1 -iat 1 -exp 1 -subtle") + assert.FatalError(t, err) + assert.True(t, strings.HasPrefix(string(out), "eyJhbGciOiJFUzI1NiIsImtpZCI6IiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiVGVzdEF1ZGllbmNlIl0sImV4cCI6MSwiaWF0IjoxLCJpc3MiOiJUZXN0SXNzdWVyIiwibmJmIjoxLCJzdWIiOiJUZXN0U3ViamVjdCJ9.")) +} + +func TestCryptoJWTVerifyWithPrivate(t *testing.T) { + exp := time.Now().Add(1 * time.Minute) + out, err := CombinedOutput(fmt.Sprintf("step crypto jwt sign -key testdata/p256.pem -iss TestIssuer -aud TestAudience -sub TestSubject -exp %d | step crypto jwt verify -key testdata/p256.pem -subtle", exp.Unix())) + assert.FatalError(t, err) + m := make(map[string]interface{}) + assert.FatalError(t, json.Unmarshal(out, &m)) + assert.Equals(t, "TestIssuer", m["payload"].(map[string]interface{})["iss"]) +} diff --git a/integration/jwk_test.go b/integration/jwk_test.go new file mode 100644 index 00000000..903957bf --- /dev/null +++ b/integration/jwk_test.go @@ -0,0 +1,419 @@ +// +build integration + +package integration + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "strconv" + "strings" + "testing" + + "github.com/smallstep/assert" + jose "gopkg.in/square/go-jose.v2" +) + +func AssertFileExists(t *testing.T, path string, a ...interface{}) bool { + info, err := os.Lstat(path) + if err != nil { + if os.IsNotExist(err) { + assert.FatalError(t, err, fmt.Sprintf("unable to find file %q", path), a) + } + assert.FatalError(t, err, fmt.Sprintf("error when running os.Lstat(%q): %s", path, err), a) + } + assert.Fatal(t, !info.IsDir(), fmt.Sprintf("%q is a directory", path), a) + assert.Equals(t, int(info.Mode()), 0600) + return true +} + +type JWKTest struct { + name string + pubfile string + prvfile string + command CLICommand +} + +func NewJWKTest(name string) JWKTest { + pubfile := fmt.Sprintf("%s/%s-pub.json", TempDirectory, name) + prvfile := fmt.Sprintf("%s/%s-prv.json", TempDirectory, name) + cmd := NewCLICommand().setCommand("step crypto jwk create").setArguments(fmt.Sprintf("%s %s", pubfile, prvfile)) + return JWKTest{name, pubfile, prvfile, cmd} +} + +func (j JWKTest) setFlag(flag, value string) JWKTest { + return JWKTest{j.name, j.pubfile, j.prvfile, j.command.setFlag(flag, value)} +} + +func (j JWKTest) cmd() string { + return j.command.cmd() +} + +func (j JWKTest) run() (CLIOutput, error) { + return j.command.run() +} + +func (j JWKTest) test(t *testing.T, msg ...interface{}) (CLIOutput, string) { + var out CLIOutput + var pass string + t.Run(j.name, func(t *testing.T) { + //fmt.Printf("Running command: %s", j.cmd()) + out, err := j.run() + assert.FatalError(t, err, fmt.Sprintf("`%s`: returned error '%s'\n\nOutput:\n%s", j.cmd(), err, out.combined)) + AssertFileExists(t, j.pubfile, fmt.Sprintf("step crypto jwk create should create public JWK at %s", j.pubfile)) + AssertFileExists(t, j.prvfile, fmt.Sprintf("step crypto jwk create should create private JWK at %s", j.prvfile)) + j.checkPublic(t) + if _, ok := j.command.flags["no-password"]; ok { + j.checkPrivate(t, "") + assert.Equals(t, out.combined, "", msg) + } else { + // Output should be look like: + // Private JWK file '' will be encrypted with the key: + // + lines := strings.Split(out.stdout, "\n") + outMsg := fmt.Sprintf("Unexpected output from `%s`:\n\n%s", j.cmd(), out.stdout) + assert.Equals(t, 3, len(lines), outMsg) + if len(lines) == 3 { + assert.Equals(t, fmt.Sprintf("Private JWK file '%s' will be encrypted with the key:", j.prvfile), lines[0]) + assert.Equals(t, "", out.stderr) + pass = lines[1] + j.checkPrivate(t, pass) + } + } + }) + return out, pass +} + +func (j JWKTest) readJson(t *testing.T, name string) map[string]interface{} { + dat, err := ioutil.ReadFile(name) + assert.FatalError(t, err) + m := make(map[string]interface{}) + assert.FatalError(t, json.Unmarshal(dat, &m)) + return m +} + +func (j JWKTest) public(t *testing.T) map[string]interface{} { + return j.readJson(t, j.pubfile) +} + +func (j JWKTest) private(t *testing.T) map[string]interface{} { + return j.readJson(t, j.prvfile) +} + +func (j JWKTest) kty() string { + // Default "kty" is EC + kty := "EC" + if v, ok := j.command.flags["kty"]; ok { + kty = v + } else if v, ok = j.command.flags["type"]; ok { + kty = v + } + return kty +} + +/* +func (j JWKTest) crv() string { + if v, ok := j.command.flags["crv"]; ok { + return v, true + } else if v, ok = j.command.flags["curve"]; ok { + return v, true + } + return "", false +} +*/ + +func (j JWKTest) checkPubPriv(t *testing.T, m map[string]interface{}) { + + checkSize := func(v string, defaultSize int) { + bytes, err := base64.RawURLEncoding.DecodeString(v) + assert.FatalError(t, err) + if v, ok := j.command.flags["size"]; ok { + size, err := strconv.Atoi(v) + assert.FatalError(t, err) + assert.Equals(t, len(bytes)*8, size) + } else { + assert.Equals(t, len(bytes)*8, defaultSize) + } + } + + checkSizeBytes := func(v string, defaultSize int) { + bytes, err := base64.RawURLEncoding.DecodeString(v) + assert.FatalError(t, err) + if v, ok := j.command.flags["size"]; ok { + size, err := strconv.Atoi(v) + assert.FatalError(t, err) + assert.Equals(t, len(bytes), size) + } else { + assert.Equals(t, len(bytes), defaultSize) + } + } + + kty := j.kty() + assert.Equals(t, kty, m["kty"]) + + if v, ok := j.command.flags["use"]; ok { + assert.Equals(t, v, m["use"]) + } else { + // Default "use" is "sig" + assert.Equals(t, "sig", m["use"]) + } + + if v, ok := j.command.flags["kid"]; ok { + assert.Equals(t, v, m["kid"]) + } else { + assert.False(t, "" == m["kid"]) + } + + if kty == "EC" { + _, ok := m["size"] + assert.True(t, !ok, "size attribute for EC key") + + if v, ok := j.command.flags["crv"]; ok { + assert.Equals(t, v, m["crv"]) + } else { + switch j.command.flags["alg"] { + case "ES256": + assert.Equals(t, "P-256", m["crv"]) + case "ES384": + assert.Equals(t, "P-384", m["crv"]) + case "ES512": + assert.Equals(t, "P-521", m["crv"]) + default: + assert.Equals(t, "P-256", m["crv"]) + } + } + + if v, ok := j.command.flags["alg"]; ok { + assert.Equals(t, v, m["alg"]) + } else { + if m["use"] == "enc" { + assert.Equals(t, "ECDH-ES", m["alg"]) + } else { + switch m["crv"] { + case "P-256": + assert.Equals(t, "ES256", m["alg"]) + case "P-384": + assert.Equals(t, "ES384", m["alg"]) + case "P-521": + assert.Equals(t, "ES512", m["alg"]) + } + } + } + + // TODO: Check EC parameters and key size + } else if kty == "OKP" { + _, ok := m["size"] + assert.True(t, !ok, "size attribute for OKP key") + assert.Equals(t, "Ed25519", m["crv"]) + assert.Equals(t, "EdDSA", m["alg"]) + _, ok = m["x"] + assert.True(t, ok, "JWK with \"kty\" of \"OKP\" should have \"x\" parameter (public key)") + } else if kty == "RSA" { + _, ok := m["crv"] + assert.True(t, !ok, "crv attribute for non-EC key") + + if v, ok := j.command.flags["alg"]; ok { + assert.Equals(t, v, m["alg"]) + } else { + // Default "alg" is "RS256" for "RSA" keys + assert.Equals(t, "RS256", m["alg"]) + } + + n, ok := m["n"] + assert.True(t, ok, "JWK with \"kty\" of \"RSA\" should have \"n\" parameter (modulus)") + _, ok = m["e"] + assert.True(t, ok, "JWK with \"kty\" of \"RSA\" should have \"e\" parameter (exponent)") + + // Check that `n` is the correct size + checkSize(n.(string), 2048) + } else if kty == "oct" { + // Should be no "crv" for non-EC keys + _, ok := m["crv"] + assert.True(t, !ok, "crv attribute for non-EC key") + + if v, ok := j.command.flags["alg"]; ok { + assert.Equals(t, v, m["alg"]) + } else { + // Default "alg" is "HS256" for "oct" keys + assert.Equals(t, "HS256", m["alg"]) + } + + k, ok := m["k"] + assert.True(t, ok, "JWK with \"kty\" of \"oct\" should have \"k\" paramater (key)") + + // Check `k` is correct size + checkSizeBytes(k.(string), 32) + } else { + assert.True(t, false, fmt.Sprintf("invalid key type: %s", kty)) + } +} + +func (j JWKTest) checkPublic(t *testing.T) { + j.checkPubPriv(t, j.public(t)) +} + +func isJWE(m map[string]interface{}) bool { + // `ciphertext` which MUST be present in a JWE according to RFC7516 + _, ok := m["ciphertext"] + return ok +} + +func (j JWKTest) decryptJWEPayload(t *testing.T, password string) map[string]interface{} { + dat, err := ioutil.ReadFile(j.prvfile) + assert.FatalError(t, err) + enc, err := jose.ParseEncrypted(string(dat)) + assert.FatalError(t, err) + dec, err := enc.Decrypt([]byte(password)) + assert.FatalError(t, err) + m := make(map[string]interface{}) + assert.FatalError(t, json.Unmarshal(dec, &m)) + return m +} + +func (j JWKTest) checkPrivate(t *testing.T, password string) { + m := j.private(t) + _, nopass := j.command.flags["no-password"] + if isJWE(m) { + assert.False(t, nopass, "expected unencrypted JWK with --no-password flag but got JWE") + hdrb, ok := m["protected"] + assert.True(t, ok, "missing protected header attribute in JWE") + hdr, err := base64.RawURLEncoding.DecodeString(hdrb.(string)) + assert.FatalError(t, err) + assert.Equals(t, string(hdr), `{"alg":"A128KW","enc":"A128GCM"}`) + m = j.decryptJWEPayload(t, password) + } else { + assert.True(t, nopass, "JWKs should be encrypted in JWE unless --no-password flag is passed") + } + j.checkPubPriv(t, m) + if j.kty() == "EC" { + // TODO: Check EC parameters and key size + } else if j.kty() == "OKP" { + _, ok := m["d"] + assert.True(t, ok, "JWK with \"kty\" of \"OKP\" should have \"d\" parameter (private key)") + } else if j.kty() == "RSA" { + d, ok := m["d"] + assert.True(t, ok, "JWK with \"kty\" of \"RSA\" should have \"d\" parameter (private exponent)") + // Check that size of `d` is the correct size + bytes, err := base64.RawURLEncoding.DecodeString(d.(string)) + assert.FatalError(t, err) + if v, ok := j.command.flags["size"]; ok { + size, err := strconv.Atoi(v) + assert.FatalError(t, err) + assert.Equals(t, len(bytes)*8, size) + } else { + assert.Equals(t, len(bytes)*8, 2048) + } + + _, ok = m["p"] + assert.True(t, ok, "JWK with \"kty\" of \"RSA\" should have \"p\" parameter (first prime factor)") + _, ok = m["q"] + assert.True(t, ok, "JWK with \"kty\" of \"RSA\" should have \"p\" parameter (second prime factor)") + } +} + +// TODO: Calling this on a successful test appears to cause a SEGFAULT? +func (j JWKTest) fail(t *testing.T, expected string, msg ...interface{}) { + j.command.fail(t, j.name, expected, msg) +} + +func TestCryptoJWK(t *testing.T) { + t.Run("jwk", func(t *testing.T) { + NewJWKTest("default").test(t) + t.Run("kty=RSA", func(t *testing.T) { + NewJWKTest("RSA-2048-RS256").setFlag("kty", "RSA").setFlag("size", "2048").setFlag("alg", "RS256").test(t) + NewJWKTest("RSA-2048-RS384").setFlag("kty", "RSA").setFlag("size", "2048").setFlag("alg", "RS384").test(t) + NewJWKTest("RSA-2048-RS512").setFlag("kty", "RSA").setFlag("size", "2048").setFlag("alg", "RS512").test(t) + NewJWKTest("RSA-4096-RS256").setFlag("kty", "RSA").setFlag("size", "4096").setFlag("alg", "RS256").test(t) + NewJWKTest("RSA-2048-PS256").setFlag("kty", "RSA").setFlag("size", "2048").setFlag("alg", "PS256").test(t) + NewJWKTest("RSA-2048-PS384").setFlag("type", "RSA").setFlag("size", "2048").setFlag("alg", "PS384").test(t) + NewJWKTest("RSA-2048-PS512").setFlag("kty", "RSA").setFlag("size", "2048").setFlag("alg", "PS512").test(t) + NewJWKTest("RSA-1024-PS256-fail").setFlag("kty", "RSA").setFlag("size", "1024").setFlag("alg", "PS512").fail(t, "minimum '--size' for RSA keys is 2048 bits without '--insecure' flag\n") + NewJWKTest("RSA-1024-PS256").setFlag("type", "RSA").setFlag("size", "1024").setFlag("alg", "PS512").setFlag("insecure", "").test(t) + NewJWKTest("RSA-16-PS256").setFlag("kty", "RSA").setFlag("size", "16").setFlag("alg", "PS512").setFlag("insecure", "").test(t) + // Broken - actual size is 16. Needs to be multiple of 8? + //NewJWKTest("RSA-12-PS256").setFlag("kty", "RSA").setFlag("size", "12").setFlag("alg", "PS512").setFlag("insecure", "").test(t) + // Broken - fails in crypto/rsa + //NewJWKTest("RSA-11-PS256").setFlag("kty", "RSA").setFlag("size", "11").setFlag("alg", "PS512").setFlag("insecure", "").test(t) + //NewJWKTest("RSA-0-PS256").setFlag("kty", "RSA").setFlag("size", "0").setFlag("alg", "PS512").setFlag("insecure", "").test(t) + NewJWKTest("RSA-0-PS256").setFlag("kty", "RSA").setFlag("size", "-1").setFlag("alg", "PS512").setFlag("insecure", "").fail(t, "flag '--size' must be >= 0\n") + NewJWKTest("RSA-2048-PS256-enc-bad-alg").setFlag("type", "RSA").setFlag("size", "2048").setFlag("alg", "PS256").setFlag("use", "enc").fail(t, "alg 'PS256' is not compatible with kty 'RSA'\n") + NewJWKTest("RSA-2048-A128KW-enc-bad-alg").setFlag("type", "RSA").setFlag("size", "2048").setFlag("alg", "A128KW").setFlag("use", "enc").fail(t, "alg 'A128KW' is not compatible with kty 'RSA'\n") + NewJWKTest("RSA-2048-RSAOAEP-enc").setFlag("type", "RSA").setFlag("size", "2048").setFlag("alg", "RSA-OAEP").setFlag("use", "enc").test(t) + NewJWKTest("RSA-2048-RSAOAEP256-enc").setFlag("kty", "RSA").setFlag("size", "2056").setFlag("alg", "RSA-OAEP-256").setFlag("use", "enc").test(t) + NewJWKTest("RSA-2048-RSA1_5-enc").setFlag("type", "RSA").setFlag("size", "2064").setFlag("alg", "RSA1_5").setFlag("use", "enc").test(t) + NewJWKTest("RSA-2048-PS512-kid-snarf").setFlag("kty", "RSA").setFlag("size", "2064").setFlag("alg", "PS512").setFlag("kid", "snarf").test(t) + NewJWKTest("RSA-default").setFlag("kty", "RSA").test(t) + NewJWKTest("alg=ES256").setFlag("kty", "RSA").setFlag("alg", "ES256").setFlag("size", "2048").fail(t, "alg 'ES256' is not compatible with kty 'RSA'\n") + NewJWKTest("alg=HS384").setFlag("type", "RSA").setFlag("alg", "HS384").setFlag("size", "2048").fail(t, "alg 'HS384' is not compatible with kty 'RSA'\n") + NewJWKTest("RSA-2048-PS256-crv").setFlag("kty", "RSA").setFlag("size", "2048").setFlag("alg", "PS256").setFlag("crv", "P-521").fail(t, "flag '--crv' is incompatible with '--kty RSA'\n") + NewJWKTest("RSA-128-PS256").setFlag("kty", "RSA").setFlag("size", "128").setFlag("alg", "PS256").fail(t, "minimum '--size' for RSA keys is 2048 bits without '--insecure' flag\n") + NewJWKTest("rsa").setFlag("kty", "rsa").fail(t, "missing or invalid value for flag '--kty'\n") + NewJWKTest("RSA-nopass-fail").setFlag("kty", "RSA").setFlag("no-password", "").fail(t, "flag '--no-password' requires the '--insecure' flag\n") + NewJWKTest("RSA-nopass").setFlag("kty", "RSA").setFlag("no-password", "").setFlag("insecure", "").test(t) + }) + t.Run("kty=oct", func(t *testing.T) { + NewJWKTest("oct-default").setFlag("kty", "oct").test(t) + NewJWKTest("oct-32-fail").setFlag("type", "oct").setFlag("size", "4").fail(t, "minimum '--size' for oct keys is 16 bytes without '--insecure' flag\n") + NewJWKTest("oct-32").setFlag("kty", "oct").setFlag("size", "4").setFlag("insecure", "").test(t) + NewJWKTest("oct-16").setFlag("kty", "oct").setFlag("size", "2").setFlag("insecure", "").test(t) + NewJWKTest("oct-0").setFlag("kty", "oct").setFlag("size", "0").setFlag("insecure", "").fail(t, "flag '--size' must be >= 0\n") + NewJWKTest("oct-512-HS256").setFlag("type", "oct").setFlag("alg", "HS256").setFlag("size", "64").test(t) + NewJWKTest("oct-512-HS384").setFlag("kty", "oct").setFlag("alg", "HS384").setFlag("size", "64").test(t) + NewJWKTest("oct-512-HS512").setFlag("kty", "oct").setFlag("alg", "HS512").setFlag("size", "64").test(t) + NewJWKTest("oct-256-HS256-enc").setFlag("kty", "oct").setFlag("alg", "HS256").setFlag("size", "32").setFlag("use", "enc").fail(t, "alg 'HS256' is not compatible with kty 'oct'\n") + NewJWKTest("oct-256-dir-enc").setFlag("kty", "oct").setFlag("alg", "dir").setFlag("size", "32").setFlag("use", "enc").test(t) + NewJWKTest("oct-256-A128KW-enc").setFlag("kty", "oct").setFlag("alg", "A128KW").setFlag("size", "32").setFlag("use", "enc").test(t) + NewJWKTest("oct-256-A192KW-enc").setFlag("kty", "oct").setFlag("alg", "A192KW").setFlag("size", "32").setFlag("use", "enc").test(t) + NewJWKTest("oct-256-A256KW-enc").setFlag("kty", "oct").setFlag("alg", "A256KW").setFlag("size", "32").setFlag("use", "enc").test(t) + NewJWKTest("oct-256-A128GCMKW-enc").setFlag("kty", "oct").setFlag("alg", "A128GCMKW").setFlag("size", "32").setFlag("use", "enc").test(t) + NewJWKTest("oct-256-A192GCMKW-enc").setFlag("kty", "oct").setFlag("alg", "A192GCMKW").setFlag("size", "32").setFlag("use", "enc").test(t) + NewJWKTest("oct-256-A256GCMKW-enc").setFlag("kty", "oct").setFlag("alg", "A256GCMKW").setFlag("size", "32").setFlag("use", "enc").test(t) + NewJWKTest("oct-256-HS256-kid-foo").setFlag("kty", "oct").setFlag("alg", "HS256").setFlag("size", "32").setFlag("kid", "foo").test(t) + NewJWKTest("alg=RS256").setFlag("kty", "oct").setFlag("alg", "RS256").setFlag("size", "64").fail(t, "alg 'RS256' is not compatible with kty 'oct'\n") + NewJWKTest("oct-512-HS256-crv").setFlag("kty", "oct").setFlag("alg", "HS256").setFlag("size", "64").setFlag("crv", "P-256").fail(t, "flag '--crv' is incompatible with '--kty oct'\n") + NewJWKTest("OCT").setFlag("kty", "OCT").fail(t, "missing or invalid value for flag '--kty'\n") + }) + t.Run("kty=EC", func(t *testing.T) { + NewJWKTest("EC-default").setFlag("kty", "EC").test(t) + NewJWKTest("EC-kid-w00t").setFlag("kty", "EC").setFlag("kid", "w00t").test(t) + NewJWKTest("EC-P256-ES256").setFlag("kty", "EC").setFlag("crv", "P-256").setFlag("alg", "ES256").test(t) + NewJWKTest("EC-P384-ES384").setFlag("type", "EC").setFlag("crv", "P-384").setFlag("alg", "ES384").test(t) + NewJWKTest("EC-P521-ES512").setFlag("kty", "EC").setFlag("crv", "P-521").setFlag("alg", "ES512").test(t) + NewJWKTest("EC-P521-RSA1_5-enc").setFlag("kty", "EC").setFlag("crv", "P-521").setFlag("alg", "RSA1_5").setFlag("use", "enc").fail(t, "alg 'RSA1_5' is not compatible with kty 'EC'\n") + NewJWKTest("EC-P521-ECDHES-enc").setFlag("kty", "EC").setFlag("crv", "P-521").setFlag("alg", "ECDH-ES").setFlag("use", "enc").test(t) + NewJWKTest("EC-P521-ECDHESA128KW-enc").setFlag("kty", "EC").setFlag("crv", "P-521").setFlag("alg", "ECDH-ES+A128KW").setFlag("use", "enc").test(t) + NewJWKTest("EC-P521-ECDHESA192KW-enc").setFlag("kty", "EC").setFlag("crv", "P-521").setFlag("alg", "ECDH-ES+A192KW").setFlag("use", "enc").test(t) + NewJWKTest("EC-P521-ECDHESA256KW-enc").setFlag("kty", "EC").setFlag("crv", "P-521").setFlag("alg", "ECDH-ES+A256KW").setFlag("use", "enc").test(t) + NewJWKTest("EC-P256-ES384").setFlag("type", "EC").setFlag("crv", "P-256").setFlag("alg", "ES384").fail(t, "alg 'ES384' is not compatible with kty 'EC' and crv 'P-256'\n") + NewJWKTest("EC-P256-ES256-size").setFlag("kty", "EC").setFlag("crv", "P-256").setFlag("alg", "ES256").setFlag("size", "2048").fail(t, "flag '--size' is incompatible with '--kty EC'\n") + NewJWKTest("EC-P256").setFlag("kty", "EC").setFlag("crv", "P-256").test(t) + NewJWKTest("EC-P384").setFlag("kty", "EC").setFlag("crv", "P-384").test(t) + NewJWKTest("EC-P521").setFlag("kty", "EC").setFlag("crv", "P-521").test(t) + NewJWKTest("ec").setFlag("kty", "ec").fail(t, "missing or invalid value for flag '--kty'\n") + }) + t.Run("kty=OKP", func(t *testing.T) { + NewJWKTest("OKP-Ed25519-default").setFlag("kty", "OKP").setFlag("crv", "Ed25519").test(t) + NewJWKTest("OKP-Ed25519-deadbeef").setFlag("kty", "OKP").setFlag("crv", "Ed25519").setFlag("kid", "deadbeef").test(t) + NewJWKTest("OKP-Ed25519-EdDSA").setFlag("type", "OKP").setFlag("crv", "Ed25519").setFlag("alg", "EdDSA").test(t) + NewJWKTest("OKP-Ed25519-EdDSA").setFlag("kty", "OKP").setFlag("crv", "Ed25519").setFlag("alg", "ES256").fail(t, "alg 'ES256' is not compatible with kty 'OKP' and crv 'Ed25519'\n") + NewJWKTest("OKP-Ed25519-EdDSA").setFlag("kty", "OKP").setFlag("crv", "Ed25519").setFlag("size", "256").fail(t, "flag '--size' is incompatible with '--kty OKP'\n") + NewJWKTest("okp").setFlag("kty", "okp").fail(t, "missing or invalid value for flag '--kty'\n") + }) + NewJWKTest("kty=FOO").setFlag("kty", "FOO").fail(t, "missing or invalid value for flag '--kty'\n") + NewJWKTest("kty=ec").setFlag("kty", "ec").fail(t, "missing or invalid value for flag '--kty'\n", "kty flag is case-sensitive") + NewJWKTest("alg=rs256").setFlag("kty", "RSA").setFlag("size", "2048").setFlag("alg", "rs256").fail(t, "alg 'rs256' is not compatible with kty 'RSA'\n", "alg flag is case-sensitive") + NewJWKTest("alg=snarf").setFlag("kty", "RSA").setFlag("size", "2048").setFlag("alg", "snarf").fail(t, "alg 'snarf' is not compatible with kty 'RSA'\n") + NewJWKTest("alg=rs256").setFlag("alg", "rs256").fail(t, "alg 'rs256' is not compatible with kty 'EC' and crv 'P-256'\n", "alg flag is case-sensitive") + // Broken - prints usage + //NewJWKTest("type-and-kty").setFlag("type", "RSA").setFlag("kty", "RSA").fail(t, "Cannot use two forms of the same flag: type kty") + + NewCLICommand().setCommand("step crypto jwk create").fail(t, "missing-args#1", "missing positional arguments 'PUB_FILE' 'PRIV_FILE'\n") + NewCLICommand().setCommand("step crypto jwk create").setArguments("foo.json").fail(t, "missing-args#2", "missing positional argument 'PRIV_FILE'\n") + NewCLICommand().setCommand("step crypto jwk create").setArguments("foo.1.json foo.2.json foo.3.json").fail(t, "too-many-args", "too many positional arguments use only 'PUB_FILE' 'PRIV_FILE'\n") + NewCLICommand().setCommand("step crypto jwk create").setArguments("foo.json foo.json").fail(t, "pub-priv-same", "positional arguments 'PUB_FILE' 'PRIV_FILE' cannot be equal\n") + // Broken - prints usage + //NewCLICommand().setCommand("step crypto jwk create").setArguments("foo.json bar.json").setFlag("size", "blort").fail(t, "non-int-size", "invalid value \"blort\" for flag -size: strconv.ParseInt: parsing \"blort\": invalid syntax") + }) +} diff --git a/integration/jwt_test.go b/integration/jwt_test.go new file mode 100644 index 00000000..b6d6b322 --- /dev/null +++ b/integration/jwt_test.go @@ -0,0 +1,743 @@ +// +build integration + +package integration + +import ( + crand "crypto/rand" + "encoding/base64" + "encoding/binary" + "encoding/json" + "encoding/pem" + "fmt" + "io/ioutil" + "math" + "math/rand" + "os/exec" + "reflect" + "regexp" + "strconv" + "strings" + "testing" + "time" + + "github.com/ThomasRooney/gexpect" + "github.com/icrowley/fake" + "github.com/pkg/errors" + "github.com/smallstep/assert" + "github.com/smallstep/cli/crypto/keys" + jose "gopkg.in/square/go-jose.v2" +) + +type JWK struct { + pubfile string + prvfile string + password string + ispem bool + iskeyset bool +} + +func (j JWK) jwk() (*jose.JSONWebKey, error) { + jwk := new(jose.JSONWebKey) + b, err := ioutil.ReadFile(j.prvfile) + if err != nil { + return nil, err + } + if enc, err := jose.ParseEncrypted(string(b)); err == nil { + b, err = enc.Decrypt([]byte(j.password)) + if err != nil { + return nil, err + } + } + if err := json.Unmarshal(b, jwk); err != nil { + return nil, err + } + return jwk, nil +} + +func (j JWK) pem() (string, error) { + jwk, err := j.jwk() + if err != nil { + return "", err + } + b, err := keys.PrivatePEM(jwk.Key, keys.InsecureEncOpts()) + if err != nil { + return "", err + } + return string(pem.EncodeToMemory(b)), err +} + +func readJSON(name string) (map[string]interface{}, error) { + dat, err := ioutil.ReadFile(name) + if err != nil { + return nil, err + } + m := make(map[string]interface{}) + err = json.Unmarshal(dat, &m) + return m, err +} + +type JWTSignTest struct { + command CLICommand + jwk JWK +} + +func NewJWTSignTest(jwk JWK) JWTSignTest { + cmd := NewCLICommand().setCommand("step crypto jwt sign").setFlag("key", jwk.prvfile) + return JWTSignTest{cmd, jwk} +} + +func (j JWTSignTest) setFlag(key, value string) JWTSignTest { + return JWTSignTest{j.command.setFlag(key, value), j.jwk} +} + +func (j JWTSignTest) exp(d time.Duration) JWTSignTest { + exp := time.Now().Add(d) + return j.setFlag("exp", strconv.Itoa(int(exp.Unix()))) +} + +func (j JWTSignTest) nbf(d time.Duration) JWTSignTest { + nbf := time.Now().Add(d) + return j.setFlag("nbf", strconv.Itoa(int(nbf.Unix()))) +} + +func (j JWTSignTest) iat(d time.Duration) JWTSignTest { + iat := time.Now().Add(d) + return j.setFlag("iat", strconv.Itoa(int(iat.Unix()))) +} + +func (j JWTSignTest) test(t *testing.T, name string) string { + var jwt string + t.Run(name, func(t *testing.T) { + // Beware. This is fragile as hell. Ugh. If the output or prompt for the + // jwt sign cubcommand changes this will need to change too. + if j.jwk.password != "" { + cmd, err := gexpect.Spawn(j.command.cmd()) + assert.FatalError(t, err) + prompt := "Please enter the password to decrypt " + j.jwk.prvfile + ": " + assert.Nil(t, cmd.ExpectTimeout(prompt, 1*time.Second)) + assert.Nil(t, cmd.SendLine(j.jwk.password)) + + var lines []string + for { + line, err := cmd.ReadLine() + line = strings.Trim(line, "\r") + if err != nil { + break + } + lines = append(lines, line) + } + + jwt = strings.Trim(strings.Join(lines, "\n"), " \r\n") + err = cmd.Wait() + if assert.Nil(t, err) { + j.checkJwt(t, jwt) + } + if t.Failed() { + t.Errorf("Output did not match for command `%s`", j.command.cmd()) + t.Errorf("Prompt:\n%s\n\n", prompt) + t.Errorf("Output:\n%s\n", jwt) + } + cmd.Wait() + cmd.Close() + } else { + out, err := j.command.run() + assert.FatalError(t, err) + jwt = out.stdout + j.checkJwt(t, jwt) + } + }) + return jwt +} + +func (j JWTSignTest) fail(t *testing.T, name, expected string) { + if j.jwk.password != "" { + t.Run(name, func(t *testing.T) { + cmd, err := gexpect.Command(j.command.cmd()) + assert.FatalError(t, err) + assert.FatalError(t, cmd.Start()) + assert.FatalError(t, cmd.ExpectTimeout("Please enter the password to decrypt "+j.jwk.prvfile+": ", 1*time.Second)) + assert.FatalError(t, cmd.SendLine(j.jwk.password)) + _, err = cmd.ReadLine() // Prompt prints a newline + assert.FatalError(t, err) + + var lines []string + for { + line, err := cmd.ReadLine() + line = strings.Trim(line, "\r") + if err != nil { + break + } + lines = append(lines, line) + } + + actual := strings.Join(lines, "\n") + "\n" + assert.Equals(t, expected, actual) + + err = cmd.Wait() + if assert.NotNil(t, err) { + assert.Equals(t, err.Error(), "exit status 1") + } + if t.Failed() { + t.Errorf("Error message did not match for command `%s`", j.command.cmd()) + } + }) + } else { + j.command.fail(t, name, expected, "") + } +} + +func decodeB64Json(s string) (map[string]interface{}, error) { + b, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + return nil, err + } + m := make(map[string]interface{}) + err = json.Unmarshal(b, &m) + if err != nil { + return nil, err + } + return m, nil +} + +func encodeB64Json(o interface{}) (string, error) { + b, err := json.Marshal(o) + if err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(b), nil +} + +func decodeJWT(jwt string) (map[string]interface{}, map[string]interface{}, string, error) { + parts := strings.Split(jwt, ".") + if len(parts) != 3 { + return nil, nil, "", errors.Errorf("invalid jwt; found %d parts", len(parts)) + } + header, err := decodeB64Json(parts[0]) + if err != nil { + return nil, nil, "", err + } + payload, err := decodeB64Json(parts[1]) + if err != nil { + return nil, nil, "", err + } + + return header, payload, strings.TrimRight(parts[2], "\n"), nil +} + +func inspectJWT(jwt string) (map[string]interface{}, error) { + out, err := NewCLICommand().setCommand(`step crypto jwt inspect --insecure`).setStdin(jwt).run() + if err != nil { + return nil, err + } + inspect := make(map[string]interface{}) + err = json.Unmarshal([]byte(out.stderr), &inspect) + return inspect, err +} + +func (j JWTSignTest) checkJwt(t *testing.T, jwt string) { + header, payload, signature, err := decodeJWT(jwt) + assert.FatalError(t, err) + + inspect, err := inspectJWT(strings.Trim(jwt, " \r\n")) + assert.FatalError(t, err) + assert.True(t, reflect.DeepEqual(header, inspect["header"])) + assert.True(t, reflect.DeepEqual(payload, inspect["payload"])) + assert.Equals(t, signature, inspect["signature"]) + + assert.Equals(t, "JWT", header["typ"]) + // TODO: Check that the correct alg is in the JWT + //assert.Equals(t, "ES256", header["alg"]) + + if sub, ok := j.command.flags["sub"]; ok { + assert.Equals(t, sub, payload["sub"]) + } else { + _, ok = payload["sub"] + assert.False(t, ok) + } + if iss, ok := j.command.flags["iss"]; ok { + assert.Equals(t, iss, payload["iss"]) + } else { + _, ok = payload["iss"] + assert.False(t, ok) + } + if jti, ok := j.command.flags["jti"]; ok { + assert.Equals(t, jti, payload["jti"]) + } else { + _, ok = payload["jti"] + assert.False(t, ok) + } + + // TODO: Check aud (may be an array) + //assert.Equals(t, j.command.flags["aud"], payload["aud"]) + + // TODO: Check additional payload claims from stdin + + if _, ok := j.command.flags["exp"]; ok { + eexp, err := strconv.Atoi(j.command.flags["exp"]) + assert.FatalError(t, err) + aexp := int(payload["exp"].(float64)) + assert.Equals(t, eexp, aexp) + } + + iat := payload["iat"].(float64) + nbf := payload["nbf"].(float64) + now := float64(time.Now().Unix()) + assert.True(t, math.Abs(now-iat) < 10) + assert.True(t, math.Abs(now-nbf) < 10) + if _, ok := j.command.flags["iat"]; ok { + eiat, err := strconv.Atoi(j.command.flags["iat"]) + assert.FatalError(t, err) + assert.Equals(t, eiat, int(iat)) + } + if _, ok := j.command.flags["nbf"]; ok { + enbf, err := strconv.Atoi(j.command.flags["nbf"]) + assert.FatalError(t, err) + assert.Equals(t, enbf, int(nbf)) + } +} + +type JWTVerifyTest struct { + command CLICommand + jwk JWK +} + +func NewJWTVerifyTest(jwk JWK) JWTVerifyTest { + cmd := NewCLICommand().setCommand("step crypto jwt verify").setFlag("key", jwk.pubfile) + return JWTVerifyTest{cmd, jwk} +} + +func (j JWTVerifyTest) setFlag(key, value string) JWTVerifyTest { + return JWTVerifyTest{j.command.setFlag(key, value), j.jwk} +} + +func (j JWTVerifyTest) test(t *testing.T, name, jwt string) { + t.Run(name, func(t *testing.T) { + out, err := j.command.setStdin(jwt).run() + assert.FatalError(t, err, fmt.Sprintf("`%s`: returned error '%s'\n\nOutput:\n%s\n\nJWT:\n%s", j.command.cmd(), err, out.combined, jwt)) + jwt := make(map[string]interface{}) + err = json.Unmarshal([]byte(out.combined), &jwt) + assert.FatalError(t, err) + + // TODO: Factor out some of this / combine with checkJwt above. + header := jwt["header"].(map[string]interface{}) + payload := jwt["payload"].(map[string]interface{}) + assert.Equals(t, "JWT", header["typ"]) + + // Alg in the header, cli flags, and JWK, respectively (might be nil if not set) + halg, shalg := header["alg"] + falg, sfalg := j.command.flags["alg"] + kalg, skalg := func() (string, bool) { + if j.jwk.ispem { + return "", false + } else if j.jwk.iskeyset { + jwks, err := readJSON(j.jwk.pubfile) + assert.FatalError(t, err) + for _, e := range jwks["keys"].([]interface{}) { + jwk := e.(map[string]interface{}) + if jwk["kid"].(string) == j.command.flags["kid"] { + kalg, skalg := jwk["alg"] + return kalg.(string), skalg + } + } + return "", false + } else { + jwk, err := readJSON(j.jwk.pubfile) + assert.FatalError(t, err) + kalg, skalg := jwk["alg"] + return kalg.(string), skalg + } + }() + if sfalg { + if shalg { + assert.Equals(t, falg, halg) + } + if skalg { + assert.Equals(t, falg, kalg) + } + } + if shalg && skalg { + assert.Equals(t, halg, kalg) + } + + if iss, ok := j.command.flags["iss"]; ok { + assert.Equals(t, iss, payload["iss"]) + } else { + _, ok = j.command.flags["subtle"] + assert.True(t, ok) + } + + if aud, ok := j.command.flags["aud"]; ok { + auds, ok := payload["aud"] + if ok { + switch auds := auds.(type) { + case string: + assert.Equals(t, aud, auds) + case []interface{}: + /* + TODO: This. + if len(auds) == 1 { + t.Errorf(`single "aud" in JWT should be string not array`) + } + */ + found := false + for _, a := range auds { + found = (a.(string) == aud) + } + assert.True(t, found) + default: + t.Errorf("unexpected type for aud: %T", auds) + } + } else { + t.Error(`no "aud" property in JWT`) + } + } + + iat, hiat := payload["iat"] + nbf, hnbf := payload["nbf"] + exp, hexp := payload["exp"] + now := float64(time.Now().Unix()) + if hiat { + assert.True(t, math.Abs(now-iat.(float64)) < 10) + } + if hnbf { + assert.True(t, math.Abs(now-nbf.(float64)) < 10) + } + if hexp { + assert.True(t, exp.(float64) > now) + } + }) +} + +func (j JWTVerifyTest) fail(t *testing.T, name string, jwt string, expected interface{}) { + j.command.setStdin(jwt).fail(t, name, expected, "") +} + +type JWTTest struct { + sign JWTSignTest + verify JWTVerifyTest +} + +func NewJWTTest(jwk JWK) JWTTest { + return JWTTest{NewJWTSignTest(jwk), NewJWTVerifyTest(jwk)} +} + +func (j JWTTest) setFlag(key, value string) JWTTest { + return j.setSFlag(key, value).setVFlag(key, value) +} + +func (j JWTTest) setSFlag(key, value string) JWTTest { + return JWTTest{j.sign.setFlag(key, value), j.verify} +} + +func (j JWTTest) exp(d time.Duration) JWTTest { + return JWTTest{j.sign.exp(d), j.verify} +} + +func (j JWTTest) nbf(d time.Duration) JWTTest { + return JWTTest{j.sign.nbf(d), j.verify} +} + +func (j JWTTest) iat(d time.Duration) JWTTest { + return JWTTest{j.sign.iat(d), j.verify} +} + +func (j JWTTest) setVFlag(key, value string) JWTTest { + return JWTTest{j.sign, j.verify.setFlag(key, value)} +} + +func (j JWTTest) test(t *testing.T, name string) { + t.Run(name, func(t *testing.T) { + jwt := j.sign.test(t, "sign") + j.verify.test(t, "verify", jwt) + if t.Failed() { + fmt.Printf("Commands:\n\t%s\n\t%s\n", j.sign.command.cmd(), j.verify.command.cmd()) + } + }) +} + +var rsrc csrc +var r = rand.New(rsrc) + +type csrc struct{} + +func (s csrc) Seed(seed int64) {} +func (s csrc) Int63() int64 { + return int64(s.Uint64() & ^uint64(1<<63)) +} +func (s csrc) Uint64() (v uint64) { + err := binary.Read(crand.Reader, binary.BigEndian, &v) + if err != nil { + panic(err) + } + return v +} + +func FakeURL() string { + scheme := []string{"http", "https"}[r.Intn(2)] + domain := fake.DomainName() + n := r.Intn(5) + path := make([]string, n) + for i := 0; i < n; i++ { + path[i] = fake.Word() + } + return scheme + "://" + domain + "/" + strings.Join(path, "/") +} + +// A principal is usually a name, URL, or email address. +func FakePrincipal() string { + return []string{fake.EmailAddress(), FakeURL(), fake.FullName()}[r.Intn(3)] +} + +func randid() string { + b := make([]byte, 10) + _, err := rand.Read(b) + if err != nil { + panic(err) + } + return base64.RawURLEncoding.EncodeToString(b) +} + +func NewJWK(kty string, t *testing.T) JWK { + jwk := NewJWKTest(fmt.Sprintf("jwt-jwk-%s", kty)) + jwk = jwk.setFlag("kty", kty).setFlag("kid", randid()) + if kty == "OKP" { + jwk = jwk.setFlag("crv", "Ed25519") + } + return JWKFromTest(t, jwk) +} + +func JWKFromTest(t *testing.T, jt JWKTest) JWK { + _, password := jt.test(t) + return JWK{jt.pubfile, jt.prvfile, password, false, false} +} + +func TestCryptoJWT(t *testing.T) { + // Generate some JWKs that we can use for testing. + jwkec := NewJWK("EC", t) + jwkrsa := NewJWK("RSA", t) + jwkoct := NewJWK("oct", t) + jwkokp := NewJWK("OKP", t) + jwknopass := JWKFromTest(t, NewJWKTest("jwt-jwk-nopass").setFlag("kty", "EC").setFlag("no-password", "").setFlag("insecure", "")) + + t.Run("jwt", func(t *testing.T) { + mkjwt := func(jwk JWK) JWTTest { + // Audience, issuer, and subject can be emails, URLs, or any other string. + aud := FakePrincipal() + iss := FakePrincipal() + sub := FakePrincipal() + return NewJWTTest(jwk).setFlag("aud", aud).setFlag("iss", iss).setSFlag("sub", sub).exp(1 * time.Minute) + } + mkjwt(jwkec).test(t, "jwt-ec") + mkjwt(jwkrsa).test(t, "jwt-rsa") + mkjwt(jwkoct).test(t, "jwt-oct") + mkjwt(jwkokp).test(t, "jwt-okp") + mkjwt(jwknopass).test(t, "jwt-nopass") + + t.Run("sign", func(t *testing.T) { + jwtrsa := mkjwt(jwkrsa).sign + jwtrsa.setFlag("alg", "RS384").fail(t, "wrong-alg", "alg RS384 does not match the alg on testdata-tmp/jwt-jwk-RSA-prv.json\n") + + mkjwt(JWKFromTest(t, NewJWKTest("jwt-jwk-enc").setFlag("use", "enc").setFlag("no-password", "").setFlag("insecure", ""))).sign.fail(t, "use-enc", "cannot sign using key with intended use \"enc\" (must be \"sig\")\n") + + subtle := NewJWTTest(jwkrsa).sign + subtle.fail(t, "no-aud-iss-sub-exp", "flag '--iss' is required unless '--subtle' is used\n") + subtle.setFlag("sub", FakePrincipal()).setFlag("iss", FakePrincipal()).exp(1*time.Minute).fail(t, "no-aud", "flag '--aud' is required unless '--subtle' is used\n") + subtle.setFlag("sub", FakePrincipal()).setFlag("aud", FakePrincipal()).exp(1*time.Minute).fail(t, "no-iss", "flag '--iss' is required unless '--subtle' is used\n") + subtle.setFlag("aud", FakePrincipal()).setFlag("iss", FakePrincipal()).exp(1*time.Minute).fail(t, "no-sub", "flag '--sub' is required unless '--subtle' is used\n") + subtle.setFlag("sub", FakePrincipal()).setFlag("aud", FakePrincipal()).setFlag("iss", FakePrincipal()).fail(t, "no-exp", "flag '--exp' is required unless '--subtle' is used\n") + subtle = subtle.setFlag("subtle", "") + subtle.test(t, "no-aud-iss-sub-exp-subtle") + subtle.setFlag("sub", FakePrincipal()).setFlag("iss", FakePrincipal()).exp(1*time.Minute).test(t, "no-aud-subtle") + subtle.setFlag("sub", FakePrincipal()).setFlag("aud", FakePrincipal()).exp(1*time.Minute).test(t, "no-iss-subtle") + subtle.setFlag("aud", FakePrincipal()).setFlag("iss", FakePrincipal()).exp(1*time.Minute).test(t, "no-sub-subtle") + subtle.setFlag("sub", FakePrincipal()).setFlag("aud", FakePrincipal()).setFlag("iss", FakePrincipal()).test(t, "no-exp-subtle") + + nouse := mkjwt(JWK{"testdata/jwk-no-use.pub.json", "testdata/jwk-no-use.json", "", false, false}) + nouse.test(t, "no-use") + noalg := mkjwt(JWK{"testdata/jwk-no-alg.pub.json", "testdata/jwk-no-alg.json", "", false, false}) + noalg.sign.test(t, "no-alg") + + mkjwt(JWK{"foo", "foo", "", false, false}).sign.fail(t, "missing-key-file", "error reading foo: open foo: no such file or directory\n") + mkjwt(JWK{"testdata/bad-key.pub.json", "testdata/bad-key.json", "", false, false}).sign.fail(t, "bad-key-file", "error reading testdata/bad-key.json: unsupported format\n") + mkjwt(JWK{"testdata/bad-key.pub.json", "testdata/bad-key.json", "", true, false}).sign.fail(t, "bad-key-file-pem", "error reading testdata/bad-key.json: unsupported format\n") + mkjwt(JWK{"testdata/jwk-pGoLJDgF5fgTNnB47SKMnVUzVNdu6MF0.pub.json", "testdata/jwk-pGoLJDgF5fgTNnB47SKMnVUzVNdu6MF0.pub.json", "", false, false}).sign.fail(t, "sign-with-pubkey", "cannot use a public key for signing\n") + mkjwt(JWK{"testdata/p256.pem.pub", "testdata/p256.pem.pub", "", true, false}).sign.fail(t, "sign-with-pubkey-pem", "cannot use a public key for signing\n") + mkjwt(JWK{"testdata/rsa2048.pub", "testdata/rsa2048.pem", "", true, false}).sign.fail(t, "pem-alg-required", "flag '--alg' is required with the given key\n") + mkjwt(JWK{"testdata/rsa2048.pub", "testdata/rsa2048.pem", "", true, false}).sign.setFlag("alg", "RS256").test(t, "pem-alg-required") + mkjwt(JWK{"testdata/rsa2048.pub", "testdata/twopems.pem", "", true, false}).sign.setFlag("alg", "RS256").fail(t, "multiple-keys", "error decoding PEM: file 'testdata/twopems.pem' contains more than one key\n") + mkjwt(JWK{"testdata/rsa2048.pub", "testdata/badheader.pem", "", true, false}).sign.setFlag("alg", "RS256").fail(t, "multiple-keys", "error decoding PEM: file 'testdata/badheader.pem' contains an unexpected header 'FOO PRIVATE KEY'\n") + mkjwt(JWK{"testdata/es256-enc.pub", "testdata/es256-enc.pem", "password", true, false}).sign.setFlag("alg", "ES256").test(t, "pem-encrypted") + mkjwt(JWK{"testdata/es256-enc.pub", "testdata/es256-enc.pem", "password", true, false}).sign.setFlag("alg", "RS256").fail(t, "pem-bad-alg", "alg RS256 does not match the alg on testdata/es256-enc.pem\n") + + mkjwt(jwkrsa).exp(-1*time.Minute).sign.fail(t, "exp-in-past", "flag '--exp' must be in the future unless '--subtle' is used\n") + mkjwt(jwkrsa).exp(-1*time.Minute).setFlag("subtle", "").sign.test(t, "exp-in-past-subtle") + + mkjwt(jwkrsa).setSFlag("jti", "foo").test(t, "jti") + + keyset := JWTSignTest{NewCLICommand().setCommand("step crypto jwt sign").setFlag("jwks", "testdata/jwks.json"), JWK{"testdata/jwks.pub.json", "testdata/jwks.json", "", false, true}} + keyset = keyset.setFlag("aud", FakePrincipal()).setFlag("iss", FakePrincipal()).setFlag("sub", FakePrincipal()).exp(1 * time.Minute) + keyset.fail(t, "keyset-no-kid", "flag `--kid` is required with `--jwks`\n") + keyset.setFlag("kid", "1").test(t, "keyset-kid1") + keyset.setFlag("kid", "2").test(t, "keyset-kid2") + keyset.setFlag("kid", "3").fail(t, "keyset-kid3", "cannot sign using key with intended use \"enc\" (must be \"sig\")\n") + keyset.setFlag("kid", "4").fail(t, "keyset-kid4", "cannot find key with kid 4 on testdata/jwks.json\n") + keyset.setFlag("kid", "1").setFlag("key", "foo").fail(t, "keyset-and-key", "flags `--key` and `--jwks` are mutually exclusive\n") + keyset.setFlag("jwks", "foo").setFlag("kid", "1").fail(t, "nonexistent-keyset", "error reading foo: open foo: no such file or directory\n") + keyset.setFlag("jwks", "testdata/rsa2048.pem").setFlag("kid", "1").fail(t, "bad-keyset", "error reading testdata/rsa2048.pem: unsupported format\n") + }) + + t.Run("verify", func(t *testing.T) { + setHeader := func(jwt string, hdr string, val interface{}) string { + parts := strings.Split(jwt, ".") + if len(parts) != 3 { + assert.FatalError(t, errors.Errorf("invalid jwt; found %d parts", len(parts))) + } + header, err := decodeB64Json(parts[0]) + if err != nil { + assert.FatalError(t, err) + } + header[hdr] = val + parts[0], err = encodeB64Json(header) + if err != nil { + assert.FatalError(t, err) + } + return strings.Join(parts, ".") + } + + tst := mkjwt(jwkokp) + jwt := tst.sign.test(t, "sign") + tst.verify.fail(t, "wrong-signature", jwt[:len(jwt)-5]+"12345", "validation failed: invalid signature\n") + tst.verify.setFlag("iss", "wrong issuer").fail(t, "iss-mismatch", jwt, "validation failed: invalid issuer claim (iss)\n") + tst.verify.setFlag("aud", "wrong audience").fail(t, "aud-mismatch", jwt, "validation failed: invalid audience claim (aud)\n") + tst.verify.fail(t, "crit-header", setHeader(jwt, "crit", []string{"exp"}), "validation failed: unrecognized critical headers (crit)\n") + tst.verify.fail(t, "invalid-jwt", "asdf", "error parsing token: compact JWS format must have three parts\n") + tst.verify.fail(t, "invalid-jwt-parts", "foo.bar.deadbeef", "error parsing token: invalid character '~' looking for beginning of value\n") + + fakejwt := func(header, payload, signature string) string { + header = base64.RawURLEncoding.EncodeToString([]byte(header)) + payload = base64.RawURLEncoding.EncodeToString([]byte(payload)) + return strings.Join([]string{header, payload, signature}, ".") + } + parts := strings.Split(jwt, ".") + tst.verify.fail(t, "invalid-jwt-header", fakejwt("foo", parts[1], parts[2]), "error parsing token: invalid character 'o' in literal false (expecting 'a')\n") + tst.verify.fail(t, "invalid-jwt-header-json", fakejwt("[42]", "bar", "deadbeef"), "error parsing token: json: cannot unmarshal array into Go value of type jose.rawHeader\n") + tst.verify.fail(t, "invalid-jwt-header-changed-attrib", fakejwt(`{"kty":"EC","alg":"ES256","xxx":"yyy"}`, parts[1], parts[2]), "validation failed: invalid signature\n") + tst.verify.fail(t, "invalid-jwt-header-bad-json", fakejwt(`{"kty":"EC","alg":"ES256","}`, parts[1], parts[2]), "error parsing token: unexpected end of JSON input\n") + tst.verify.fail(t, "invalid-jwt-payload", fakejwt(parts[0], "foo", parts[2]), "error parsing token: invalid character 'e' looking for beginning of value\n") + + subtle := NewJWTTest(jwkokp).exp(1 * time.Minute).verify + subtle.fail(t, "no-aud-iss", jwt, "flag '--iss' is required unless '--subtle' is used\n") + subtle.setFlag("iss", tst.verify.command.flags["iss"]).fail(t, "no-aud", jwt, "flag '--aud' is required unless '--subtle' is used\n") + subtle.setFlag("aud", tst.verify.command.flags["aud"]).fail(t, "no-iss", jwt, "flag '--iss' is required unless '--subtle' is used\n") + subtle = subtle.setFlag("subtle", "") + subtle.test(t, "no-aud-iss-subtle", jwt) + subtle.setFlag("iss", tst.verify.command.flags["iss"]).test(t, "no-aud-subtle", jwt) + subtle.setFlag("aud", tst.verify.command.flags["aud"]).test(t, "no-iss-subtle", jwt) + + t.Run("keyset", func(t *testing.T) { + aud := FakePrincipal() + sub := FakePrincipal() + iss := FakePrincipal() + jwt = JWTSignTest{NewCLICommand().setCommand("step crypto jwt sign").setFlag("jwks", "testdata/jwks.json"), JWK{"testdata/jwks.pub.json", "testdata/jwks.json", "", false, true}}.setFlag("kid", "1").setFlag("aud", aud).setFlag("iss", iss).setFlag("sub", sub).exp(1*time.Minute).test(t, "keyset-sign") + keyset := JWTVerifyTest{NewCLICommand().setCommand("step crypto jwt verify").setFlag("jwks", "testdata/jwks.pub.json"), JWK{"testdata/jwks.pub.json", "testdata/jwks.json", "", false, true}}.setFlag("aud", aud).setFlag("iss", iss) + keyset.setFlag("kid", "1").test(t, "keyset", jwt) + keyset.setFlag("kid", "2").fail(t, "wrong-kid", jwt, "validation failed: invalid signature\n") + keyset.setFlag("kid", "4").fail(t, "kid-not-found", jwt, "cannot find key with kid 4 on testdata/jwks.pub.json\n") + // "kid" should be optional if it's in the JWT, else required + keyset.test(t, "kid-in-jwt", jwt) + jwt = mkjwt(JWK{"testdata/jwks-key1.json", "testdata/jwks-key1.json", "", false, false}).sign.setFlag("aud", aud).setFlag("iss", iss).setFlag("sub", sub).setFlag("no-kid", "").exp(1*time.Minute).test(t, "keyset-key1") + keyset.fail(t, "no-kid-in-jwt", jwt, "flag `--kid` is required with `--jwks`\n") + }) + + // JWK without `alg` should require --alg flag + mkossljwt := func(t *testing.T, header, payload, key string) string { + cmd := fmt.Sprintf("./openssl-jwt.sh -a RS256 -k %s '%s' '%s'", key, header, payload) + jwt, err := exec.Command("bash", "-c", cmd).CombinedOutput() + assert.FatalError(t, err) + return string(jwt) + } + t.Run("pem", func(t *testing.T) { + exp := time.Now().Add(1 * time.Minute) + jwt = mkossljwt(t, `{"typ": "JWT", "alg": "RS256"}`, fmt.Sprintf(`{"iss": "foo", "aud": "bar", "exp": %d}`, exp.Unix()), "testdata/rsa2048.pem") + vtst := NewJWTVerifyTest(JWK{"testdata/rsa2048.pub", "testdata/rsa2048.pem", "", true, false}) + vtst.setFlag("iss", "foo").setFlag("aud", "bar").fail(t, "no-alg", jwt, "flag '--alg' is required with the given key\n") + vtst.setFlag("iss", "foo").setFlag("aud", "bar").setFlag("alg", "RS256").test(t, "verify", jwt) + vtst.setFlag("iss", "foo").setFlag("aud", "bar").setFlag("alg", "RS384").fail(t, "alg-mismatch", jwt, "alg RS384 does not match the alg on JWT (RS256)\n") + jwt = mkossljwt(t, `{"typ": "JWT", "alg": "RS384"}`, `{"iss": "foo", "aud": "bar"}`, "testdata/rsa2048.pem") + vtst.setFlag("iss", "foo").setFlag("aud", "bar").setFlag("alg", "RS384").fail(t, "wrong-alg", jwt, "validation failed: invalid signature\n") + vtst.setFlag("iss", "foo").setFlag("aud", "bar").setFlag("alg", "RS256").fail(t, "wrong-alg-mismatch", jwt, "alg RS256 does not match the alg on JWT (RS384)\n") + }) + expiredZero := mkossljwt(t, `{"typ": "JWT", "alg": "RS256"}`, `{"exp": 0}`, "testdata/rsa2048.pem") + NewJWTVerifyTest(JWK{"testdata/rsa2048.pub", "testdata/rsa2048.pem", "", true, false}).setFlag("subtle", "").setFlag("alg", "RS256").fail(t, "expired-zero", expiredZero, regexp.MustCompile(`^validation failed: token is expired by (\d+h\d+m\d+\.\d+s) \(exp\)\n`)) + expired := mkossljwt(t, `{"typ": "JWT", "alg": "RS256"}`, `{"exp": 12345}`, "testdata/rsa2048.pem") + NewJWTVerifyTest(JWK{"testdata/rsa2048.pub", "testdata/rsa2048.pem", "", true, false}).setFlag("subtle", "").setFlag("alg", "RS256").fail(t, "expired", expired, regexp.MustCompile(`^validation failed: token is expired by (\d+h\d+m\d+\.\d+s) \(exp\)\n`)) + noexp := mkossljwt(t, `{"typ": "JWT", "alg": "RS256"}`, `{"iss": "foo", "aud": "bar"}`, "testdata/rsa2048.pem") + NewJWTVerifyTest(JWK{"testdata/rsa2048.pub", "testdata/rsa2048.pem", "", true, false}).setFlag("no-exp-check", "").setFlag("iss", "foo").setFlag("aud", "bar").setFlag("alg", "RS256").setFlag("no-exp-check", "").fail(t, "no-exp-fail", noexp, "flag '--no-exp-check' requires the '--insecure' flag\n") + NewJWTVerifyTest(JWK{"testdata/rsa2048.pub", "testdata/rsa2048.pem", "", true, false}).setFlag("no-exp-check", "").setFlag("iss", "foo").setFlag("aud", "bar").setFlag("alg", "RS256").setFlag("no-exp-check", "").setFlag("insecure", "").test(t, "no-exp", noexp) + texp := NewJWTTest(jwkrsa).setSFlag("sub", FakePrincipal()).setFlag("aud", FakePrincipal()).setFlag("iss", FakePrincipal()) + jwt = texp.sign.setFlag("subtle", "").test(t, "no-exp-sign") + texp.verify.fail(t, "no-exp-verify-fail", jwt, "jwt must have \"exp\" property unless '--subtle' is used\n") + texp.verify.setFlag("no-exp-check", "").setFlag("insecure", "").test(t, "empty-exp-in-jwt", jwt) + texp.verify.setFlag("subtle", "").test(t, "no-exp-verify-subtle", jwt) + + // Can't serialize OKP (Ed25519) keys yet. Switch to using RSA. + tst = mkjwt(jwkrsa) + pem, err := tst.verify.jwk.pem() + assert.FatalError(t, err) + jwt = mkossljwt(t, `{"typ": "JWT", "alg": "RS384"}`, `{"iss": "foo", "sub": "bar"}`, fmt.Sprintf("<(echo -en %q)", pem)) + tst.verify.setFlag("iss", "foo").setFlag("aud", "bar").setFlag("alg", "RS384").fail(t, "wrong-alg", jwt, "alg RS384 does not match the alg on testdata-tmp/jwt-jwk-RSA-pub.json\n") + + // We don't currently support JSON Serialization, Flattened JSON Serialzation, or multiple signatures + // TODO: Right now these are parse failures. They should probably parse correctly and give more helpful error messages. + vtst := NewJWTVerifyTest(JWK{"testdata/rsa2048.pub", "testdata/rsa2048.pem", "", true, false}).setFlag("iss", "foo").setFlag("aud", "bar").setFlag("alg", "RS256") + jwtb, _ := ioutil.ReadFile("testdata/jwt-json-serialization.json") + vtst.fail(t, "json-serialization", string(jwtb), "error parsing token: unexpected end of JSON input\n") + jwtb, _ = ioutil.ReadFile("testdata/jwt-json-serialization-flattened.json") + vtst.fail(t, "json-serialization-flattened", string(jwtb), "error parsing token: unexpected end of JSON input\n") + jwtb, _ = ioutil.ReadFile("testdata/jwt-json-serialization-multi.json") + vtst.fail(t, "json-serialization-multi", string(jwtb), "error parsing token: unexpected end of JSON input\n") + }) + + // Should fail (token not yet valid) + t.Run("timestamps", func(t *testing.T) { + t.Parallel() + mkjwt(jwkrsa).iat(1*time.Second).test(t, "iat") + t.Run("nbf", func(t *testing.T) { + tst := mkjwt(jwkec).nbf(1 * time.Second) + jwt := tst.nbf(1*time.Second).sign.test(t, "sign") + tst.verify.fail(t, "verify-tosoon", jwt, "validation failed: token not valid yet (nbf)\n") + time.Sleep(1 * time.Second) + tst.verify.test(t, "verify-succeed", jwt) + if t.Failed() { + t.Logf("jwt: %s", jwt) + } + }) + t.Run("exp", func(t *testing.T) { + tst := mkjwt(jwkec).exp(1 * time.Second) + jwt := tst.sign.test(t, "sign") + tst.verify.test(t, "verify-succeed", jwt) + time.Sleep(1 * time.Second) + tst.verify.fail(t, "verify-expired", jwt, regexp.MustCompile(`^validation failed: token is expired by (\d\d\dms|\d\.\d+s) \(exp\)\n`)) + if t.Failed() { + t.Logf("jwt: %s", jwt) + } + }) + }) + + t.Run("wrong-pass", func(t *testing.T) { + tst := mkjwt(jwkrsa).setFlag("aud", "a").setFlag("iss", "i").setSFlag("sub", "s").exp(1 * time.Minute) + cmd, err := gexpect.Spawn(tst.sign.command.cmd()) + assert.FatalError(t, err) + prompt := "Please enter the password to decrypt " + tst.sign.jwk.prvfile + ": " + assert.FatalError(t, cmd.ExpectTimeout(prompt, 1*time.Second)) + assert.FatalError(t, cmd.SendLine("foo")) + assert.FatalError(t, cmd.ExpectTimeout(fmt.Sprintf("failed to decrypt %s: square/go-jose: error in cryptographic primitive", tst.sign.jwk.prvfile), 1*time.Second)) + // TODO: This should re-prompt for the password, not fail! + //assert.FatalError(t, cmd.SendLine(t.jwk.password)) + }) + + t.Run("inspect", func(t *testing.T) { + NewCLICommand().setCommand(`echo "foo" | step crypto jwt inspect`).fail(t, "requires-insecure", "'step crypto jwt inspect' must include the '--insecure' flag\n", "") + }) + }) +} diff --git a/integration/keypair_test.go b/integration/keypair_test.go new file mode 100644 index 00000000..1a594637 --- /dev/null +++ b/integration/keypair_test.go @@ -0,0 +1,101 @@ +// +build integration + +package integration + +import ( + "fmt" + "testing" + "time" + + "github.com/ThomasRooney/gexpect" + "github.com/smallstep/assert" +) + +type KeypairCmd struct { + name string + command CLICommand + pubfile string + prvfile string + password string +} + +func (k KeypairCmd) setFlag(key, value string) KeypairCmd { + return KeypairCmd{k.name, k.command.setFlag(key, value), k.pubfile, k.prvfile, k.password} +} + +func (k KeypairCmd) setPassword(password string) KeypairCmd { + return KeypairCmd{k.name, k.command, k.pubfile, k.prvfile, password} +} + +func (k KeypairCmd) testJwtSignVerify(t *testing.T) { + aud := FakePrincipal() + iss := FakePrincipal() + sub := FakePrincipal() + jwk := JWK{k.pubfile, k.prvfile, k.password, true, false} + test := NewJWTTest(jwk).setFlag("aud", aud).setFlag("iss", iss).setSFlag("sub", sub).exp(1 * time.Minute) + if k.command.flags["type"] == "RSA" { + test = test.setFlag("alg", "RS256") + } + test.test(t, fmt.Sprintf("%s-jwt-sign-verify", k.name)) +} + +func (k KeypairCmd) test(t *testing.T) { + t.Run(k.name, func(t *testing.T) { + cmd, err := gexpect.Spawn(k.command.cmd()) + assert.FatalError(t, err) + prompt := fmt.Sprintf("Password with which to encrypt private key file `%s`: ", k.prvfile) + assert.FatalError(t, cmd.ExpectTimeout(prompt, 10*time.Second)) + assert.FatalError(t, cmd.SendLine(k.password)) + k.testJwtSignVerify(t) + }) +} + +func (k KeypairCmd) testNoPass(t *testing.T) { + k.command.test(t, k.name, "", "") + k.testJwtSignVerify(t) +} + +func (k KeypairCmd) fail(t *testing.T, expected string) { + k.command.fail(t, k.name, expected, "") +} + +func (k KeypairCmd) failNoPass(t *testing.T, expected string) { + k.command.fail(t, k.name, expected, "") +} + +func NewKeypairCmd(name string) KeypairCmd { + pubfile := fmt.Sprintf("%s/%s.pub", TempDirectory, name) + prvfile := fmt.Sprintf("%s/%s.pem", TempDirectory, name) + command := NewCLICommand().setCommand(fmt.Sprintf("step crypto keypair %s %s", pubfile, prvfile)) + return KeypairCmd{name, command, pubfile, prvfile, "password"} +} + +func TestCryptoKeypair(t *testing.T) { + NewCLICommand().setCommand("step crypto keypair").fail(t, "no-args", "missing positional arguments 'PUB_FILE' 'PRIV_FILE'\n", "") + NewCLICommand().setCommand("step crypto keypair foo").fail(t, "no-args", "missing positional argument 'PRIV_FILE'\n", "") + NewKeypairCmd("default").test(t) + t.Run("RSA", func(t *testing.T) { + NewKeypairCmd("RSA-default").setFlag("type", "RSA").test(t) + NewKeypairCmd("RSA-size-0-fail").setFlag("type", "RSA").setFlag("size", "0").fail(t, "minimum '--size' for RSA keys is 2048 bits without '--insecure' flag\n") + NewKeypairCmd("RSA-size-16-fail").setFlag("type", "RSA").setFlag("size", "16").fail(t, "minimum '--size' for RSA keys is 2048 bits without '--insecure' flag\n") + NewKeypairCmd("RSA-size-neg1-fail").setFlag("type", "RSA").setFlag("size", "-1").setFlag("insecure", "").fail(t, "--size must be >= 0\n") + // Error when signing JWT: "error serializing JWT: crypto/rsa: message too long for RSA public key size" + //NewKeypairCmd("RSA-size-16").setFlag("type", "RSA").setFlag("size", "16").setFlag("insecure", "").test(t) + NewKeypairCmd("RSA-size-1024-fail").setFlag("type", "RSA").setFlag("size", "1024").fail(t, "minimum '--size' for RSA keys is 2048 bits without '--insecure' flag\n") + NewKeypairCmd("RSA-size-1024").setFlag("type", "RSA").setFlag("size", "1024").setFlag("insecure", "").test(t) + NewKeypairCmd("RSA-size-3072").setFlag("type", "RSA").setFlag("size", "3072").test(t) + NewKeypairCmd("RSA-size-4096").setFlag("type", "RSA").setFlag("size", "4096").test(t) + NewKeypairCmd("RSA-curve").setFlag("type", "RSA").setFlag("crv", "P-256").fail(t, "key type 'RSA' is not compatible with flag '--crv'\n") + }) + t.Run("EC", func(t *testing.T) { + NewKeypairCmd("EC-default").setFlag("type", "EC").test(t) + NewKeypairCmd("P-256").setFlag("type", "EC").setFlag("crv", "P-256").test(t) + NewKeypairCmd("P-384").setFlag("type", "EC").setFlag("curve", "P-384").test(t) + NewKeypairCmd("P-521").setFlag("type", "EC").setFlag("crv", "P-521").test(t) + NewKeypairCmd("bad-crv").setFlag("type", "EC").setFlag("curve", "P-512").fail(t, "invalid value for argument crv (crv: 'P-512')\n") + NewKeypairCmd("EC-size").setFlag("type", "EC").setFlag("size", "2048").fail(t, "key type 'EC' is not compatible with flag '--size'\n") + }) + NewKeypairCmd("bad-type").setFlag("type", "foo").fail(t, "unrecognized key type: foo\n") + NewKeypairCmd("no-pass-fail").setFlag("no-password", "").failNoPass(t, "flag '--no-password' requires the '--insecure' flag\n") + NewKeypairCmd("no-pass").setPassword("").setFlag("no-password", "").setFlag("insecure", "").testNoPass(t) +} diff --git a/integration/openssl-jwt.sh b/integration/openssl-jwt.sh new file mode 100755 index 00000000..0957ba88 --- /dev/null +++ b/integration/openssl-jwt.sh @@ -0,0 +1,41 @@ +#! /usr/bin/env bash + +ALG="RS256" + +while getopts "a:k:s:" opt; do + case "$opt" in + k) KEY=$OPTARG ;; + s) KEY=<(echo -en "$OPTARG") ;; + a) ALG=$OPTARG ;; + esac +done + +shift $((OPTIND-1)) + +HEADER=$1 +PAYLOAD=$2 + +function b64enc() { openssl enc -base64 -A | tr '+/' '-_' | tr -d '='; } + +function convert_ec { + INPUT=$(openssl asn1parse -inform der) + R=$(echo "$INPUT" | head -2 | tail -1 | cut -d':' -f4) + S=$(echo "$INPUT" | head -3 | tail -1 | cut -d':' -f4) + + echo -n $R | xxd -r -p + echo -n $S | xxd -r -p +} + +function rs_sign() { openssl dgst -binary -sha${1} -sign "$2"; } +function es_sign() { openssl dgst -binary -sha${1} -sign "$2" | convert_ec; } + +JWT_HDR_B64="$(echo -n "$HEADER" | b64enc)" +JWT_PAY_B64="$(echo -n "$PAYLOAD" | b64enc)" +UNSIGNED_JWT="$JWT_HDR_B64.$JWT_PAY_B64" + +case "$ALG" in + RS*) SIGNATURE=$(echo -n "$UNSIGNED_JWT" | rs_sign "${ALG#RS}" "$KEY" | b64enc) ;; + ES*) SIGNATURE=$(echo -n "$UNSIGNED_JWT" | es_sign "${ALG#ES}" "$KEY" | b64enc) ;; +esac + +echo "$UNSIGNED_JWT.$SIGNATURE" diff --git a/integration/otp_test.go b/integration/otp_test.go new file mode 100644 index 00000000..e1bd3c4e --- /dev/null +++ b/integration/otp_test.go @@ -0,0 +1,73 @@ +// +build integration + +package integration + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" + "github.com/smallstep/assert" +) + +const ( + totpSecretFile = "testdata/totp.secret" + totpSecret = "UPCTJYT7MUR4RWOUJ3TGTUB43IYCBJ76" + totpUrlFile = "testdata/totp.url" + totpUrl = "otpauth://totp/example.com:foo@example.com?algorithm=SHA1&digits=6&issuer=example.com&period=30&secret=EW32D2CFTAIRTEAWTRQZZXAITVA4U6K4" +) + +func mkotp(subcommand string, flags map[string]string) CLICommand { + return CLICommand{fmt.Sprintf("step crypto otp %s", subcommand), "", flags, nil} +} + +func TestCryptoOtp(t *testing.T) { + c := mkotp("generate", map[string]string{"issuer": "example.com", "account": "foo@example.com"}) + t.Run("generate", func(t *testing.T) { + out, err := c.run() + assert.Nil(t, err) + assert.Equals(t, len(strings.TrimSuffix(out.combined, "\n")), 32) + }) + + c = mkotp("generate", map[string]string{"issuer": "example.com", "account": "foo@example.com", "url": ""}) + t.Run("generate-url", func(t *testing.T) { + out, err := c.run() + assert.Nil(t, err) + assert.True(t, strings.HasPrefix(out.combined, "otpauth://")) + key, err := otp.NewKeyFromURL(out.combined) + assert.Nil(t, err) + assert.Equals(t, key.Type(), "totp") + assert.Equals(t, key.Issuer(), "example.com") + assert.Equals(t, key.AccountName(), "foo@example.com") + assert.True(t, len(key.Secret()) == 32) + }) + + c = mkotp("verify", map[string]string{"secret": totpSecretFile}) + t.Run("verify", func(t *testing.T) { + code, err := totp.GenerateCode(totpSecret, time.Now()) + assert.Nil(t, err) + out, err := c.setStdin(code).run() + assert.Nil(t, err) + assert.Equals(t, "Enter Passcode: ok\n", out.combined) + out, err = c.setStdin("foo").run() + assert.NotNil(t, err) + assert.Equals(t, "Enter Passcode: fail\n", out.combined) + }) + + c = mkotp("verify", map[string]string{"secret": totpUrlFile}) + t.Run("verify-url", func(t *testing.T) { + key, err := otp.NewKeyFromURL(totpUrl) + assert.FatalError(t, err) + code, err := totp.GenerateCode(key.Secret(), time.Now()) + assert.Nil(t, err) + out, err := c.setStdin(code).run() + assert.Nil(t, err) + assert.Equals(t, "Enter Passcode: ok\n", out.combined) + out, err = c.setStdin("foo").run() + assert.NotNil(t, err) + assert.Equals(t, "Enter Passcode: fail\n", out.combined) + }) +} diff --git a/integration/testdata/bad-key.json b/integration/testdata/bad-key.json new file mode 100644 index 00000000..b33c1389 --- /dev/null +++ b/integration/testdata/bad-key.json @@ -0,0 +1 @@ +foobarbaz diff --git a/integration/testdata/bad-key.pub.json b/integration/testdata/bad-key.pub.json new file mode 100644 index 00000000..b33c1389 --- /dev/null +++ b/integration/testdata/bad-key.pub.json @@ -0,0 +1 @@ +foobarbaz diff --git a/integration/testdata/badheader.pem b/integration/testdata/badheader.pem new file mode 100644 index 00000000..b57db0b0 --- /dev/null +++ b/integration/testdata/badheader.pem @@ -0,0 +1,27 @@ +-----BEGIN FOO PRIVATE KEY----- +MIIEpAIBAAKCAQEAn4V0GxrIhjoheOpXLbJNg8GdjxCRhOGbY6HZRRWCzKaEq0+7 +iLh9z3v5ixWH6rnftTBsZeFKiikqb0tRRZymuOtUz42ZuzAoDiAwFQg/sYgNdkQQ +zJHYNlbd7FF2YQyJ0JAw+cmvqbLzO0E+mgZImTNzCqzBBhLoUWjQqr1al7lXUAGw +UvSrI7JdIMOfWEvQ2wgPqeoLL3AE97zEoiYyIaTrInQlNcA5Cs1mRa396ET6hw8b +FfPZAXsW3m8JeGuqxJ23wL4jCee/w9v+qNL6oKwtpuCBOOnS5MEYH5GhvlBQ7baN +zoTAT7uk9yc1qVd7aLr2RJ5yW2v2v5LazmUzeQIDAQABAoIBACPe7ZHevvK4Bajc +AUiMTLPxCM4P6rkXxkpsLaBESwpb839WSZRf8CKE/UNSTyLwMybaQbXTKGDTCvDF +3fuqUy9H8+VMMSKPnKI4iLdiCHiSYHyUp7ZooVbux66JTvZZzG+yzOCOgsrFK77K +WBpoiVCx6g+fczQ7cjREPo/2TnXJY6ZQEntNeDnWN4CgJWDYTruPYTgTj84WUwPi +4aTDzPwZcps45ZRBLlBmd2tIxy87jBSKzGvwKK9c7++mtiwr956d7HsosaqSSJCa +jDO9JRlzcqAq8r+GZF1mmyoI3P6jgYWCElq+s1zZ0RZBaVfdhEl+AkR5pI3+oGdk +h8AdzZECgYEA07YASLNHYm99H21e1X8Yh4zZK96MfTESvAHkcIXEU7y0zw9tczPL +KTB6eXr9mY0JjSgVw94nx8YLYPU/dr0xqBZGD57FvqYRWdwdswk0CXSyEgWRccnB +jfV391gauBi2gQLxzRZf7eNl2c15kabpV0KUi/Pfu645PB2bY1yD0/UCgYEAwOR7 +GAog8y4TsPYxNpNAdgnlFU/XpE3sJPIy6Q7cGN4cuWBsdaPToTkAQiQYa5UnC5ql +dKHNwkVTwUvTa/pnRCShkdgNd2ycYDErbMe+DolEt1Ffr5+jVD4MILdGJq5CqGok +PrLhH9ZU0mfFlc/4Hkt1VkcoWPt8uT4eL0GPsvUCgYEAhriCZcDv5AveK2mFt4Yx +LdDLQcdUzzWzHkB2BcSZsk+bH0hJ9c03svZOeY9yYYwGT/T6JLHxzoaQJxrpT74F +I1lJLBd07mTvFaeknpF0s6+2wREaBLbGnHdf594A4rWXLXGaPU/Hq7HQ1lCS08TL +J+QOcyC1dtDfSwnsH8Z3fSECgYAS8t78v5H5EZ+xlJ3FBLYiYlp0u4EtjNIT1w8V +QfZxIvCjbUt6SvuxLM5PsQgNGXvacfiq+nIiEXlm1bIRO2oFkauljhnUj4DVGj9v +0jdjaiyr7Xx+3inHTskWNarYhenabYLd/eiLnhx7BuKsEuAG6da/AQJ/q0TXVbjV +X5VkOQKBgQC5m6Rw2gsDXliMSmlZNbTpLRqyJPZ5agDjSUzBo7q2RkD7XOawgDGl +LmL4bq52AdgstsAHooOerkrsmfVxgzE1ep2SfP8Pl/3yPcwEUw/AUJJxusxWKiEx +Iz8+iP0AJqiUuDuTOkTZ7V7YVCbdmTRP+Vp3R4uVZEM1I+0+oyDvYg== +-----END FOO PRIVATE KEY----- diff --git a/integration/testdata/es256-enc.pem b/integration/testdata/es256-enc.pem new file mode 100644 index 00000000..2b404753 --- /dev/null +++ b/integration/testdata/es256-enc.pem @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,a4865e47e33d1aedfc31de42301cedbd + +PI3whVCk8RTo8OW8GhlyoFsI1++0izc1QyE4qOTDK9nCjnu7xy1p5JgixVVrqFB0 +FGkKWQK46UKwJTjHV3zczblwpV2VAIb6GE9V24tkfZc9SCFFNO8kOakajU61vidk +twu0O952g31IKzSYAebLLO5dGdF6fODilyuOv0FEVkY= +-----END EC PRIVATE KEY----- diff --git a/integration/testdata/es256-enc.pub b/integration/testdata/es256-enc.pub new file mode 100644 index 00000000..19c9bd20 --- /dev/null +++ b/integration/testdata/es256-enc.pub @@ -0,0 +1,4 @@ +-----BEGIN ECDSA Public Key----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElxUERVm5N//HdLhWXUbimi91qhQ6 +O+CHTcGmd2KkoJonBKIhd+xeKN4/Rl345kZkuMzwCApqAXBjjrTARh48Bg== +-----END ECDSA Public Key----- diff --git a/integration/testdata/es256.pem b/integration/testdata/es256.pem new file mode 100644 index 00000000..44677d11 --- /dev/null +++ b/integration/testdata/es256.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIP9i6mrfUyym7wT529j7lPTbJXxqm5eTIKT8QIL2pdhGoAoGCCqGSM49 +AwEHoUQDQgAEePFeGCwt5mdsQeYQv7ZYO8hpB//WzYwuO2hu5cr5sNHOk+FWX2fh +wZrBnOW8uMuHj5paHMgmPXtD5zL94j+qqw== +-----END EC PRIVATE KEY----- diff --git a/integration/testdata/es256.pub b/integration/testdata/es256.pub new file mode 100644 index 00000000..48e8c3c3 --- /dev/null +++ b/integration/testdata/es256.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEePFeGCwt5mdsQeYQv7ZYO8hpB//W +zYwuO2hu5cr5sNHOk+FWX2fhwZrBnOW8uMuHj5paHMgmPXtD5zL94j+qqw== +-----END PUBLIC KEY----- diff --git a/integration/testdata/jwk-no-alg.json b/integration/testdata/jwk-no-alg.json new file mode 100644 index 00000000..9ff6d1ae --- /dev/null +++ b/integration/testdata/jwk-no-alg.json @@ -0,0 +1,8 @@ +{ + "use": "sig", + "kty": "EC", + "crv": "P-256", + "x": "X6YL64nzq2AyDj46j9MAGkjDJSD_gqgitMhXNG-VQpw", + "y": "xemItlL_CTNelUGQ1WZIIvNKWP2Dv3a9nDudBIlniQ0", + "d": "2WzbC-0-Rq6IwMYbYOvCqd74zrHMxk1HbTsh634Vt-I" +} diff --git a/integration/testdata/jwk-no-alg.pub.json b/integration/testdata/jwk-no-alg.pub.json new file mode 100644 index 00000000..ca1738dd --- /dev/null +++ b/integration/testdata/jwk-no-alg.pub.json @@ -0,0 +1,7 @@ +{ + "use": "sig", + "kty": "EC", + "crv": "P-256", + "x": "X6YL64nzq2AyDj46j9MAGkjDJSD_gqgitMhXNG-VQpw", + "y": "xemItlL_CTNelUGQ1WZIIvNKWP2Dv3a9nDudBIlniQ0" +} diff --git a/integration/testdata/jwk-no-use.json b/integration/testdata/jwk-no-use.json new file mode 100644 index 00000000..cd9c37bb --- /dev/null +++ b/integration/testdata/jwk-no-use.json @@ -0,0 +1,8 @@ +{ + "kty": "EC", + "crv": "P-256", + "alg": "ES256", + "x": "yqDzNseI3CwBh3R456SqjVAjkiyoNgxMp7rcEewmvHc", + "y": "s6Br4pT_lQimrZAPMe2STgpv0wKnf6-wlAqXUui5OPs", + "d": "IBcTKwlgszkE0Rr_q27-D0bS2ONToUtDUVIJ_Tulwig" +} diff --git a/integration/testdata/jwk-no-use.pub.json b/integration/testdata/jwk-no-use.pub.json new file mode 100644 index 00000000..2190987b --- /dev/null +++ b/integration/testdata/jwk-no-use.pub.json @@ -0,0 +1,7 @@ +{ + "kty": "EC", + "crv": "P-256", + "alg": "ES256", + "x": "yqDzNseI3CwBh3R456SqjVAjkiyoNgxMp7rcEewmvHc", + "y": "s6Br4pT_lQimrZAPMe2STgpv0wKnf6-wlAqXUui5OPs" +} diff --git a/integration/testdata/jwk-pGoLJDgF5fgTNnB47SKMnVUzVNdu6MF0.json b/integration/testdata/jwk-pGoLJDgF5fgTNnB47SKMnVUzVNdu6MF0.json new file mode 100644 index 00000000..3de119e7 --- /dev/null +++ b/integration/testdata/jwk-pGoLJDgF5fgTNnB47SKMnVUzVNdu6MF0.json @@ -0,0 +1,7 @@ +{ + "protected": "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIn0", + "encrypted_key": "apdmKYEs70rRLyhSFahbfpu5tZS8u3Lc", + "iv": "5RE1AoHlM6Z4-cxf", + "ciphertext": "6bch64RagQmGbNAk9yrig04QcYbTLL-QTmR4U8mi2XjWIOKJ3bS1YjHgD68ef1Iljha_cCtnMkm2JKf1n5Q4wVE7gJpX3WCDJtlI8iKrpze2XFFBt8964Rtk6d1Weo6fyZkzD7ZFf3LfAbGy2gGXeaqfc7lZNc2WYEmwUwyHE8vP8qXWfXtwIA--6J45fMt2iLeMVydAh6LS4oqQLWX5e7dRhTu-x1EHW-iVXJOj0QJsetlUWSwsN3xozJyOEPM2CK0jJoEJH88gMQ", + "tag": "nTqC8HHoZrGtvcFSxNcakA" +} \ No newline at end of file diff --git a/integration/testdata/jwk-pGoLJDgF5fgTNnB47SKMnVUzVNdu6MF0.pub.json b/integration/testdata/jwk-pGoLJDgF5fgTNnB47SKMnVUzVNdu6MF0.pub.json new file mode 100644 index 00000000..30b44243 --- /dev/null +++ b/integration/testdata/jwk-pGoLJDgF5fgTNnB47SKMnVUzVNdu6MF0.pub.json @@ -0,0 +1,8 @@ +{ + "use": "sig", + "kty": "EC", + "crv": "P-256", + "alg": "ES256", + "x": "588xYzTh9Qp1f06BOBWVohArMyauc4wTzQRx_Z7WinI", + "y": "_A_PTsoN2Y5Z3FOxXuzJNoSheGkQIie9sr_9Vl02kOo" +} \ No newline at end of file diff --git a/integration/testdata/jwks-key1.json b/integration/testdata/jwks-key1.json new file mode 100644 index 00000000..9a90d7e8 --- /dev/null +++ b/integration/testdata/jwks-key1.json @@ -0,0 +1,10 @@ +{ + "use": "sig", + "kty": "EC", + "kid": "1", + "crv": "P-256", + "alg": "ES256", + "x": "CYxl_HfIf80PTI7z3WR9JlZy_Z2isFmXHraXhgHAos0", + "y": "xsGV1k-WVTI2LeF2tU88aIkQiR4zAVuRkas5lb5xPTw", + "d": "Ko2frTowTbG-VUowa3wqqOAfhJtvByZ08EyYXerGkLc" +} diff --git a/integration/testdata/jwks.json b/integration/testdata/jwks.json new file mode 100644 index 00000000..7bffe68c --- /dev/null +++ b/integration/testdata/jwks.json @@ -0,0 +1,36 @@ +{ + "keys": [ + { + "use": "sig", + "kty": "EC", + "kid": "1", + "crv": "P-256", + "alg": "ES256", + "x": "CYxl_HfIf80PTI7z3WR9JlZy_Z2isFmXHraXhgHAos0", + "y": "xsGV1k-WVTI2LeF2tU88aIkQiR4zAVuRkas5lb5xPTw", + "d": "Ko2frTowTbG-VUowa3wqqOAfhJtvByZ08EyYXerGkLc" + }, + { + "use": "sig", + "kty": "RSA", + "kid": "2", + "alg": "RS256", + "n": "zqY_VnA7ba9rp5E-jpjBRwAr8i0XKDyCnfAhI9fi66hV1pir4gZF6j11fwGSMi-Bqp2Ky_9_w23-dO4BOSPlL5JJ7AjGWmEpI_YzTGIA_GkT7s6xXLe016T6a5K4Nw8jh1xO073kxPkRpNBiQIIGHM-oFpnJZOZu0MzHOMzG2rjdRm1qTc-d6WpsEs2vDixAaGLkq5Arz-AAL2TTKY3-SZDYrv862HiFIDZRDrd3IFdAas_S-sPQDi2F3LZfMgblF8RkYLtQstJkaMUoAv5JgTg4pq3jpOuvb0axHloNdCKYO0wmxcJLyqsE9BryX4QF4edjev-swQQ_UrWALsp3fw", + "e": "AQAB", + "d": "xqS8gcTE-70CyUMvVTe1oyChHd0GQ6FXFac81A20jj90tCJyJ4VMM8z8YygJdsB_7pgeUda65AuZ7KDVAC5nIwGGmaQdt8dqGq2Rxjz3IauIg6igibX12s02A6438oVU68tp4tTJUibyNPIzYDsc0Rk0RPVzyNBRedk_MHR4Osp7PkDQ6ERCB-WFSTYZoTyFcx9YaNZikIsoOF2mfJjV86haUevsrX1npW17LqWNLG7RDo2fO3c_I6L1121FmK5xXsntYYRDBzmzWgCQo1h6Vp8lqSwcYsvz07Ms664ZvzZg1FrJyihOIMGVhwCc1pzLBWYBqTP4GEeOjoLWnTLmkQ", + "p": "3dYQ4eqJiLW9_ifnMU14bHQGonSZb80F89joncv1f59UyM25NBBaB7ceiNwbUarPFXsYwdycJ6VggGllXPcaamkJ65rPGeDDxyZQ66gAKnu3dkOnh3N906EsjNzHxlGUH3gt58HV4lq5Y98tZTUO0X2U1jO24goFWqFqXY-KsT0", + "q": "7nlweOi8DoEer3ohFdDuk7XLLEt3Mi-DF3tyyyyPBCURilcTsSnhoOQDSm-s0OdBplOlGTEdI4RqtUo2Y0kv2IGsgBcEjZKjIyYjmlCZ_Jkg1yijwQxoffo8sjYRNA4VYCqJ8qG0k5jbiclkjRiJ7Cf7TTbxJHRvWs40XcOMH2s" + }, + { + "use": "enc", + "kty": "RSA", + "kid": "3", + "alg": "RS256", + "n": "szcBbFtJHcz6qve6zi9vBB08kP2s-CjI2nls05rbij7oyN5G7St7XayQFAkyr78u25IKB3oav43-wjHraIoD8IYQyoKztEfabPu8vPyHVUsjBULRPWU1aeMKOctFsD-nOsQTqMGXdXWF1LDC3gcy8VJ6-eH5oJGgDiY8BLyRi2meNjt5sJyC_r5KkRM_rE2EG8JpPVafDTC84nTt_AqpVt7qKlnkskICX7rL2QBc91nDrQM2RoLyLIoC0cZCoJXXm8KGhmqa-vcf0oPVWi6ypMBIiSW33gm5NfaKkZOFmLZb6WTfahvFgAvOGAM2860qStaEUuyvVsNqruXCVmhu6Q", + "e": "AQAB", + "d": "T2lgSBNrIrlhmcCMFjEOkFQkMls1-gCYf7auclV8UpXtsJRN-Wn7EFcWwMoSm62rpb_gkc-ZaqgQ6xwTpA8ED-BYMGQaHRh5wTELQPLlRPY2Xm5tKTdfo7vnHBTmnGKYR4H69BxUcMfStZxdvOSTvjs-ItvvMSdWNO7cSX0FQTCd8fUSZzHbcX0r9Moyu4SjHK78VruBaBCfpeiVjOHvc6R2jmc4oXizZDBvHCCZNks3jgZvSnOUx-CapHV9BMJY6Pd64zOcyyEH3jbma4Y9bh9p_WEZVbtex9Xgo9wR51J8TlgnnY1HmnzsT3Loh_Q0ey9IJzpxFyMqCEp2x0_vAQ", + "p": "z_AMFhSk0EfNjci_OkBt-qrvQCWA6QKnY0pxEnm8L6N2nNr3TFwwddeRcYtROR6bfnR-VrI6KbzBMSpLlRVXXY7TQU7T7cADU-alh7D4g9F7jWfTpPBNoKu_kuL5RuqkgPEtGYbCqWTa1DOExjLSdYad6kP1mjSrn5Tn4sPqbF0", + "q": "3KNj9gx8QkVSXUbK_5aLoEdWrXxVATAJfll1cLA4yVfVYeCu1uJl_j44IjJ29NE6jTbYHVmVYM6pV-Lb2J0pn1QE0QqbnQVfq8TzR0rhedLfh2Ts4lkrw-Iv1yfKbpshesSySkffDw-6A4QGVtiUNtEqC2cYuCtdUbNFHDTrQ_0" + } + ] +} diff --git a/integration/testdata/jwks.pub.json b/integration/testdata/jwks.pub.json new file mode 100644 index 00000000..70392da2 --- /dev/null +++ b/integration/testdata/jwks.pub.json @@ -0,0 +1,29 @@ +{ + "keys": [ + { + "use": "sig", + "kty": "RSA", + "kid": "2", + "alg": "RS256", + "n": "zqY_VnA7ba9rp5E-jpjBRwAr8i0XKDyCnfAhI9fi66hV1pir4gZF6j11fwGSMi-Bqp2Ky_9_w23-dO4BOSPlL5JJ7AjGWmEpI_YzTGIA_GkT7s6xXLe016T6a5K4Nw8jh1xO073kxPkRpNBiQIIGHM-oFpnJZOZu0MzHOMzG2rjdRm1qTc-d6WpsEs2vDixAaGLkq5Arz-AAL2TTKY3-SZDYrv862HiFIDZRDrd3IFdAas_S-sPQDi2F3LZfMgblF8RkYLtQstJkaMUoAv5JgTg4pq3jpOuvb0axHloNdCKYO0wmxcJLyqsE9BryX4QF4edjev-swQQ_UrWALsp3fw", + "e": "AQAB" + }, + { + "use": "sig", + "kty": "EC", + "kid": "1", + "crv": "P-256", + "alg": "ES256", + "x": "CYxl_HfIf80PTI7z3WR9JlZy_Z2isFmXHraXhgHAos0", + "y": "xsGV1k-WVTI2LeF2tU88aIkQiR4zAVuRkas5lb5xPTw" + }, + { + "use": "enc", + "kty": "RSA", + "kid": "3", + "alg": "RS256", + "n": "szcBbFtJHcz6qve6zi9vBB08kP2s-CjI2nls05rbij7oyN5G7St7XayQFAkyr78u25IKB3oav43-wjHraIoD8IYQyoKztEfabPu8vPyHVUsjBULRPWU1aeMKOctFsD-nOsQTqMGXdXWF1LDC3gcy8VJ6-eH5oJGgDiY8BLyRi2meNjt5sJyC_r5KkRM_rE2EG8JpPVafDTC84nTt_AqpVt7qKlnkskICX7rL2QBc91nDrQM2RoLyLIoC0cZCoJXXm8KGhmqa-vcf0oPVWi6ypMBIiSW33gm5NfaKkZOFmLZb6WTfahvFgAvOGAM2860qStaEUuyvVsNqruXCVmhu6Q", + "e": "AQAB" + } + ] +} diff --git a/integration/testdata/jwt-json-serialization-flattened.json b/integration/testdata/jwt-json-serialization-flattened.json new file mode 100644 index 00000000..62b2b923 --- /dev/null +++ b/integration/testdata/jwt-json-serialization-flattened.json @@ -0,0 +1,6 @@ +{ + "payload": "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiJ9", + "protected": "eyJleHAiOiA0Njg2MzA2MTI5LCAiYXVkIjogImZvbyIsICJpc3MiOiAiYmFyIiwgInN1YiI6ICJiYXoifQ", + "header": {"kid": "rsa2048.pem"}, + "signature": "IUYTIU6gUDCFc5Ou7MDogK1LWP4qbo9esx6rCpBlDzQFV1TN83J1g7L9EwRnYZ8zI1FyHIHhLMwNgyQgvYOONUJQVs0ia7aXV_Z6Uno4cQPVoB5FDWmfY4GTi5GsADS2kyT0BRVCFp1QAT0uVAC03Iqh0NtUZs51C_Bj8tGY2NFvUuRZ3sHuVUhC7tn2Aw86dm2z7YLRH6lir2tjFnNwEimqb_i5BBjkxNUIBpRBNWf7FjH80q5RKk2sGnzFDqR2XqGNxkpHZ5iEjq5MOl97tLEfwcqU4FkNbEn5qG31-Xq39BBg5uaX-YeV4P7duCiA0DY-Yxf6aRNOkCmprrmPBA" +} diff --git a/integration/testdata/jwt-json-serialization-multi.json b/integration/testdata/jwt-json-serialization-multi.json new file mode 100644 index 00000000..eecb60ca --- /dev/null +++ b/integration/testdata/jwt-json-serialization-multi.json @@ -0,0 +1,15 @@ +{ + "payload": "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiJ9", + "signatures": [ + { + "protected": "eyJleHAiOiA0Njg2MzA2MTI5LCAiYXVkIjogImZvbyIsICJpc3MiOiAiYmFyIiwgInN1YiI6ICJiYXoifQ", + "header": {"kid": "rsa2048.pem"}, + "signature": "IUYTIU6gUDCFc5Ou7MDogK1LWP4qbo9esx6rCpBlDzQFV1TN83J1g7L9EwRnYZ8zI1FyHIHhLMwNgyQgvYOONUJQVs0ia7aXV_Z6Uno4cQPVoB5FDWmfY4GTi5GsADS2kyT0BRVCFp1QAT0uVAC03Iqh0NtUZs51C_Bj8tGY2NFvUuRZ3sHuVUhC7tn2Aw86dm2z7YLRH6lir2tjFnNwEimqb_i5BBjkxNUIBpRBNWf7FjH80q5RKk2sGnzFDqR2XqGNxkpHZ5iEjq5MOl97tLEfwcqU4FkNbEn5qG31-Xq39BBg5uaX-YeV4P7duCiA0DY-Yxf6aRNOkCmprrmPBA" + }, + { + "protected": "eyJleHAiOiA0Njg2MzA2MTI5LCAiYXVkIjogImZvbyIsICJpc3MiOiAiYmFyIiwgInN1YiI6ICJiYXoifQ", + "header": {"kid": "es256.pem"}, + "signature": "YVyl6DjiAdh7yBmcN2W0GpbR9Ti2S8WDE-JRDSlrKGl0ibxjJUpbm8Qs5G1PNVws8MKm1Tnk7GIXsZd1ZVLrnA" + } + ] +} diff --git a/integration/testdata/jwt-json-serialization.json b/integration/testdata/jwt-json-serialization.json new file mode 100644 index 00000000..c93de939 --- /dev/null +++ b/integration/testdata/jwt-json-serialization.json @@ -0,0 +1,10 @@ +{ + "payload": "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiJ9", + "signatures": [ + { + "protected": "eyJleHAiOiA0Njg2MzA2MTI5LCAiYXVkIjogImZvbyIsICJpc3MiOiAiYmFyIiwgInN1YiI6ICJiYXoifQ", + "header": {"kid": "rsa2048.pem"}, + "signature": "IUYTIU6gUDCFc5Ou7MDogK1LWP4qbo9esx6rCpBlDzQFV1TN83J1g7L9EwRnYZ8zI1FyHIHhLMwNgyQgvYOONUJQVs0ia7aXV_Z6Uno4cQPVoB5FDWmfY4GTi5GsADS2kyT0BRVCFp1QAT0uVAC03Iqh0NtUZs51C_Bj8tGY2NFvUuRZ3sHuVUhC7tn2Aw86dm2z7YLRH6lir2tjFnNwEimqb_i5BBjkxNUIBpRBNWf7FjH80q5RKk2sGnzFDqR2XqGNxkpHZ5iEjq5MOl97tLEfwcqU4FkNbEn5qG31-Xq39BBg5uaX-YeV4P7duCiA0DY-Yxf6aRNOkCmprrmPBA" + } + ] +} diff --git a/integration/testdata/p256.pem b/integration/testdata/p256.pem new file mode 100644 index 00000000..613ce024 --- /dev/null +++ b/integration/testdata/p256.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEICyzqgCTvJmpbaAbZuJCHMK4Sgbiz12qUG4owl6/EIJnoAoGCCqGSM49 +AwEHoUQDQgAE5Eo8nuVDXtH1uErxhlOBQIllDfLWY8vgnxpDKsEm4DR1poxKgJAd +liu4JZ8+KIaVOuLggm1dkrtVCAMMRqc5mA== +-----END EC PRIVATE KEY----- diff --git a/integration/testdata/p256.pem.pub b/integration/testdata/p256.pem.pub new file mode 100644 index 00000000..2182e729 --- /dev/null +++ b/integration/testdata/p256.pem.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5Eo8nuVDXtH1uErxhlOBQIllDfLW +Y8vgnxpDKsEm4DR1poxKgJAdliu4JZ8+KIaVOuLggm1dkrtVCAMMRqc5mA== +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/integration/testdata/rsa2048.pem b/integration/testdata/rsa2048.pem new file mode 100644 index 00000000..7ef6bacf --- /dev/null +++ b/integration/testdata/rsa2048.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAn4V0GxrIhjoheOpXLbJNg8GdjxCRhOGbY6HZRRWCzKaEq0+7 +iLh9z3v5ixWH6rnftTBsZeFKiikqb0tRRZymuOtUz42ZuzAoDiAwFQg/sYgNdkQQ +zJHYNlbd7FF2YQyJ0JAw+cmvqbLzO0E+mgZImTNzCqzBBhLoUWjQqr1al7lXUAGw +UvSrI7JdIMOfWEvQ2wgPqeoLL3AE97zEoiYyIaTrInQlNcA5Cs1mRa396ET6hw8b +FfPZAXsW3m8JeGuqxJ23wL4jCee/w9v+qNL6oKwtpuCBOOnS5MEYH5GhvlBQ7baN +zoTAT7uk9yc1qVd7aLr2RJ5yW2v2v5LazmUzeQIDAQABAoIBACPe7ZHevvK4Bajc +AUiMTLPxCM4P6rkXxkpsLaBESwpb839WSZRf8CKE/UNSTyLwMybaQbXTKGDTCvDF +3fuqUy9H8+VMMSKPnKI4iLdiCHiSYHyUp7ZooVbux66JTvZZzG+yzOCOgsrFK77K +WBpoiVCx6g+fczQ7cjREPo/2TnXJY6ZQEntNeDnWN4CgJWDYTruPYTgTj84WUwPi +4aTDzPwZcps45ZRBLlBmd2tIxy87jBSKzGvwKK9c7++mtiwr956d7HsosaqSSJCa +jDO9JRlzcqAq8r+GZF1mmyoI3P6jgYWCElq+s1zZ0RZBaVfdhEl+AkR5pI3+oGdk +h8AdzZECgYEA07YASLNHYm99H21e1X8Yh4zZK96MfTESvAHkcIXEU7y0zw9tczPL +KTB6eXr9mY0JjSgVw94nx8YLYPU/dr0xqBZGD57FvqYRWdwdswk0CXSyEgWRccnB +jfV391gauBi2gQLxzRZf7eNl2c15kabpV0KUi/Pfu645PB2bY1yD0/UCgYEAwOR7 +GAog8y4TsPYxNpNAdgnlFU/XpE3sJPIy6Q7cGN4cuWBsdaPToTkAQiQYa5UnC5ql +dKHNwkVTwUvTa/pnRCShkdgNd2ycYDErbMe+DolEt1Ffr5+jVD4MILdGJq5CqGok +PrLhH9ZU0mfFlc/4Hkt1VkcoWPt8uT4eL0GPsvUCgYEAhriCZcDv5AveK2mFt4Yx +LdDLQcdUzzWzHkB2BcSZsk+bH0hJ9c03svZOeY9yYYwGT/T6JLHxzoaQJxrpT74F +I1lJLBd07mTvFaeknpF0s6+2wREaBLbGnHdf594A4rWXLXGaPU/Hq7HQ1lCS08TL +J+QOcyC1dtDfSwnsH8Z3fSECgYAS8t78v5H5EZ+xlJ3FBLYiYlp0u4EtjNIT1w8V +QfZxIvCjbUt6SvuxLM5PsQgNGXvacfiq+nIiEXlm1bIRO2oFkauljhnUj4DVGj9v +0jdjaiyr7Xx+3inHTskWNarYhenabYLd/eiLnhx7BuKsEuAG6da/AQJ/q0TXVbjV +X5VkOQKBgQC5m6Rw2gsDXliMSmlZNbTpLRqyJPZ5agDjSUzBo7q2RkD7XOawgDGl +LmL4bq52AdgstsAHooOerkrsmfVxgzE1ep2SfP8Pl/3yPcwEUw/AUJJxusxWKiEx +Iz8+iP0AJqiUuDuTOkTZ7V7YVCbdmTRP+Vp3R4uVZEM1I+0+oyDvYg== +-----END RSA PRIVATE KEY----- diff --git a/integration/testdata/rsa2048.pub b/integration/testdata/rsa2048.pub new file mode 100644 index 00000000..e60e592e --- /dev/null +++ b/integration/testdata/rsa2048.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn4V0GxrIhjoheOpXLbJN +g8GdjxCRhOGbY6HZRRWCzKaEq0+7iLh9z3v5ixWH6rnftTBsZeFKiikqb0tRRZym +uOtUz42ZuzAoDiAwFQg/sYgNdkQQzJHYNlbd7FF2YQyJ0JAw+cmvqbLzO0E+mgZI +mTNzCqzBBhLoUWjQqr1al7lXUAGwUvSrI7JdIMOfWEvQ2wgPqeoLL3AE97zEoiYy +IaTrInQlNcA5Cs1mRa396ET6hw8bFfPZAXsW3m8JeGuqxJ23wL4jCee/w9v+qNL6 +oKwtpuCBOOnS5MEYH5GhvlBQ7baNzoTAT7uk9yc1qVd7aLr2RJ5yW2v2v5LazmUz +eQIDAQAB +-----END PUBLIC KEY----- diff --git a/integration/testdata/rsa_priv.pem b/integration/testdata/rsa_priv.pem new file mode 100644 index 00000000..5c333d14 --- /dev/null +++ b/integration/testdata/rsa_priv.pem @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,76589C25A98A26E05C00F9EA8B258C36 + +jb53BmdoS97e3MXLDkwSR7foObkKjpU+LrODHVl0Z2xFdb2Gtqg5OBZgwsdDbKO8 +ouCXxQk76cgZ/331xAtczjj77aBhRjmaBMupSZrb/CQHtfbJ2LxjdfyHiGDNdklG +NLsbG+nwPITB1mf2YTd7Fo8o4qm7SCVvoVNRGbmzVuAnZw58ziPoq/FsUWXbsV4+ +EuDb3YfhBjW88wHEkbFrtpKtbF23c5YSIOL1jAanZBdJ6ML8b+DT0DOQQVEWLtZP +c3t7Ft3KRH5MKUpkS/Ge085PuL16S//1r/BbdPTipO6dH+WffMynN3y+dAfjCAiY +mY+/UMegi1/mFIt/tnA4iOb4kU5D7cpSrPkx4Sln3tTPgilw77Y12cN4VI95gOLX +i++Ud3usZrXp6GceU91XGadY+o6ZaE/61GBWxZZRH6VpAPHezYpAHc3IKz02O/8E +rZ8JpajxLmthndSWbvrC7NcErWfhC+wM4sDNgzYos8j4CUwrWzko9CyToSwz1mw6 +xSidqEY3c9B56EziFYJYytzY/tqcLPxQLxQDijfeMjVZ9lNTQEkf02DYcRzR9yQv +gkrzM+tNpetTGUocct0D9u5oNYr/N+DANsFp+udHEh/OWwgtBeeMAJ1ET0kgWj+0 +Bho5+YurvK+E7yU2uilKh5CuRYQHQTGrdH5lrBJEOF9JDSAqvQ6Uah9Pdd4rPaHG +V6SrJHg9rymuiI7TND7qMxtAlh8oseNHhG2R3mJVcmNEh3Ipoj7uAVW/T7X/qbcC +Db214mW9umaQnp0PnUE+uRPjhYH+7YOwv2LfOmBHkPJ2Zx79hoY0VnCPdSkz4wfH +FDRW0CO1piEU+DDvceGKF7zTPD6AhdY/y12s8Bpr/v9ZlDb/UWTqfAYH6SNJpl/y +KCe4G1QDUTTLWsZMS12hxPzxfb7MR8EatAPD/92DJLLeYTr4JLc9v/LwMjfCNUyg +2KXLl7dy95adiSpnPb98qZsuiane1nIN7AGbdXTY2ZJb9bdQcYahqw5dmANoK+fL +PBEnC9iXZLKL7N5aA1EyFj7V3yXPJv6iiz/zxvKbTr650gDGA9g95UPSqVqAYAIj +yAmhqdf9lfCBzuFeoVutYnD0IE6ifWE7Zuc/V/1OV+0d7eOux5BX2Fn8zyzVa+Uq +KF+0eAPKe+zvLzLMYGq/mMDqXtCTG4jk0F/xRRpWJdertKWCNx4iX4BJ9ABhbRib +DkM4+A0/8HMoqB1njE4wCLhLPb9Oskn2jmnth5EzFPbbXMH6IGKz+eAZxAeuulRG +QKkkELbMR1OsXiYjQfcDNRpsudvZIoD2ky2pNUxcsQN8lxIkcTOjDqLha39dvF3z +jHlLX7j6lMtkj7QJBZJYMTbTvL/n7axvASfhrwOR95bfxhss/lGDDL11uWqtuRfw +WEDTlrmkjwR0eWz/oZIyHPomH9OOspglx9mYndHqkHVy5/enYrPEi3N/CevcA0nL +ycGZouJvSHYg81nHpl03OxlQYaGW3cUHRq3JBQ9cPyebEz/85t+bmRj1K9IpJPPN +K1vWUhH8OIdNsE/4XAVpr82atguLVW+frGf3q5+5wDwWGwv2nh+wGxWhzXlN2qva +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/integration/testdata/rsa_pub.pem b/integration/testdata/rsa_pub.pem new file mode 100644 index 00000000..a45cd32b --- /dev/null +++ b/integration/testdata/rsa_pub.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyEU5ZhIhFn7v4bpMKlkz +dmLCj9KfmqFWig29c6OzYoMUnbdodOmZ6RId/Gw5fnluH12eFxsItlXKDT4RPSm7 +m4D1sYgFmk88oo6z4XDuItDncoIg89jGK38OZ8A0gwEoy5JqukONGmAldzgzQyiq +uzSNMeT1WO9zXCwOljcUio697M1kP/YN1Lp7n7YILVwdV8wQ2vyNKQK1M/5OZOFl +lOqww4wsqLTDK0rfxp6LAVtczp1XdxbsnpdixrK38O+dHWe4IS5HhKLmmmdTfpFQ +D3PIAXs/Naap/0+t0lsOplNPiF4BYyNBIqyyfm1o5ZQpGfmITKvDFZMBkQ2i2cou +nQIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/integration/testdata/totp.secret b/integration/testdata/totp.secret new file mode 100644 index 00000000..82089873 --- /dev/null +++ b/integration/testdata/totp.secret @@ -0,0 +1 @@ +UPCTJYT7MUR4RWOUJ3TGTUB43IYCBJ76 diff --git a/integration/testdata/totp.url b/integration/testdata/totp.url new file mode 100644 index 00000000..2b95e8fd --- /dev/null +++ b/integration/testdata/totp.url @@ -0,0 +1 @@ +otpauth://totp/example.com:foo@example.com?algorithm=SHA1&digits=6&issuer=example.com&period=30&secret=EW32D2CFTAIRTEAWTRQZZXAITVA4U6K4 diff --git a/integration/testdata/twopems.pem b/integration/testdata/twopems.pem new file mode 100644 index 00000000..55218e7c --- /dev/null +++ b/integration/testdata/twopems.pem @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAn4V0GxrIhjoheOpXLbJNg8GdjxCRhOGbY6HZRRWCzKaEq0+7 +iLh9z3v5ixWH6rnftTBsZeFKiikqb0tRRZymuOtUz42ZuzAoDiAwFQg/sYgNdkQQ +zJHYNlbd7FF2YQyJ0JAw+cmvqbLzO0E+mgZImTNzCqzBBhLoUWjQqr1al7lXUAGw +UvSrI7JdIMOfWEvQ2wgPqeoLL3AE97zEoiYyIaTrInQlNcA5Cs1mRa396ET6hw8b +FfPZAXsW3m8JeGuqxJ23wL4jCee/w9v+qNL6oKwtpuCBOOnS5MEYH5GhvlBQ7baN +zoTAT7uk9yc1qVd7aLr2RJ5yW2v2v5LazmUzeQIDAQABAoIBACPe7ZHevvK4Bajc +AUiMTLPxCM4P6rkXxkpsLaBESwpb839WSZRf8CKE/UNSTyLwMybaQbXTKGDTCvDF +3fuqUy9H8+VMMSKPnKI4iLdiCHiSYHyUp7ZooVbux66JTvZZzG+yzOCOgsrFK77K +WBpoiVCx6g+fczQ7cjREPo/2TnXJY6ZQEntNeDnWN4CgJWDYTruPYTgTj84WUwPi +4aTDzPwZcps45ZRBLlBmd2tIxy87jBSKzGvwKK9c7++mtiwr956d7HsosaqSSJCa +jDO9JRlzcqAq8r+GZF1mmyoI3P6jgYWCElq+s1zZ0RZBaVfdhEl+AkR5pI3+oGdk +h8AdzZECgYEA07YASLNHYm99H21e1X8Yh4zZK96MfTESvAHkcIXEU7y0zw9tczPL +KTB6eXr9mY0JjSgVw94nx8YLYPU/dr0xqBZGD57FvqYRWdwdswk0CXSyEgWRccnB +jfV391gauBi2gQLxzRZf7eNl2c15kabpV0KUi/Pfu645PB2bY1yD0/UCgYEAwOR7 +GAog8y4TsPYxNpNAdgnlFU/XpE3sJPIy6Q7cGN4cuWBsdaPToTkAQiQYa5UnC5ql +dKHNwkVTwUvTa/pnRCShkdgNd2ycYDErbMe+DolEt1Ffr5+jVD4MILdGJq5CqGok +PrLhH9ZU0mfFlc/4Hkt1VkcoWPt8uT4eL0GPsvUCgYEAhriCZcDv5AveK2mFt4Yx +LdDLQcdUzzWzHkB2BcSZsk+bH0hJ9c03svZOeY9yYYwGT/T6JLHxzoaQJxrpT74F +I1lJLBd07mTvFaeknpF0s6+2wREaBLbGnHdf594A4rWXLXGaPU/Hq7HQ1lCS08TL +J+QOcyC1dtDfSwnsH8Z3fSECgYAS8t78v5H5EZ+xlJ3FBLYiYlp0u4EtjNIT1w8V +QfZxIvCjbUt6SvuxLM5PsQgNGXvacfiq+nIiEXlm1bIRO2oFkauljhnUj4DVGj9v +0jdjaiyr7Xx+3inHTskWNarYhenabYLd/eiLnhx7BuKsEuAG6da/AQJ/q0TXVbjV +X5VkOQKBgQC5m6Rw2gsDXliMSmlZNbTpLRqyJPZ5agDjSUzBo7q2RkD7XOawgDGl +LmL4bq52AdgstsAHooOerkrsmfVxgzE1ep2SfP8Pl/3yPcwEUw/AUJJxusxWKiEx +Iz8+iP0AJqiUuDuTOkTZ7V7YVCbdmTRP+Vp3R4uVZEM1I+0+oyDvYg== +-----END RSA PRIVATE KEY----- +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAn4V0GxrIhjoheOpXLbJNg8GdjxCRhOGbY6HZRRWCzKaEq0+7 +iLh9z3v5ixWH6rnftTBsZeFKiikqb0tRRZymuOtUz42ZuzAoDiAwFQg/sYgNdkQQ +zJHYNlbd7FF2YQyJ0JAw+cmvqbLzO0E+mgZImTNzCqzBBhLoUWjQqr1al7lXUAGw +UvSrI7JdIMOfWEvQ2wgPqeoLL3AE97zEoiYyIaTrInQlNcA5Cs1mRa396ET6hw8b +FfPZAXsW3m8JeGuqxJ23wL4jCee/w9v+qNL6oKwtpuCBOOnS5MEYH5GhvlBQ7baN +zoTAT7uk9yc1qVd7aLr2RJ5yW2v2v5LazmUzeQIDAQABAoIBACPe7ZHevvK4Bajc +AUiMTLPxCM4P6rkXxkpsLaBESwpb839WSZRf8CKE/UNSTyLwMybaQbXTKGDTCvDF +3fuqUy9H8+VMMSKPnKI4iLdiCHiSYHyUp7ZooVbux66JTvZZzG+yzOCOgsrFK77K +WBpoiVCx6g+fczQ7cjREPo/2TnXJY6ZQEntNeDnWN4CgJWDYTruPYTgTj84WUwPi +4aTDzPwZcps45ZRBLlBmd2tIxy87jBSKzGvwKK9c7++mtiwr956d7HsosaqSSJCa +jDO9JRlzcqAq8r+GZF1mmyoI3P6jgYWCElq+s1zZ0RZBaVfdhEl+AkR5pI3+oGdk +h8AdzZECgYEA07YASLNHYm99H21e1X8Yh4zZK96MfTESvAHkcIXEU7y0zw9tczPL +KTB6eXr9mY0JjSgVw94nx8YLYPU/dr0xqBZGD57FvqYRWdwdswk0CXSyEgWRccnB +jfV391gauBi2gQLxzRZf7eNl2c15kabpV0KUi/Pfu645PB2bY1yD0/UCgYEAwOR7 +GAog8y4TsPYxNpNAdgnlFU/XpE3sJPIy6Q7cGN4cuWBsdaPToTkAQiQYa5UnC5ql +dKHNwkVTwUvTa/pnRCShkdgNd2ycYDErbMe+DolEt1Ffr5+jVD4MILdGJq5CqGok +PrLhH9ZU0mfFlc/4Hkt1VkcoWPt8uT4eL0GPsvUCgYEAhriCZcDv5AveK2mFt4Yx +LdDLQcdUzzWzHkB2BcSZsk+bH0hJ9c03svZOeY9yYYwGT/T6JLHxzoaQJxrpT74F +I1lJLBd07mTvFaeknpF0s6+2wREaBLbGnHdf594A4rWXLXGaPU/Hq7HQ1lCS08TL +J+QOcyC1dtDfSwnsH8Z3fSECgYAS8t78v5H5EZ+xlJ3FBLYiYlp0u4EtjNIT1w8V +QfZxIvCjbUt6SvuxLM5PsQgNGXvacfiq+nIiEXlm1bIRO2oFkauljhnUj4DVGj9v +0jdjaiyr7Xx+3inHTskWNarYhenabYLd/eiLnhx7BuKsEuAG6da/AQJ/q0TXVbjV +X5VkOQKBgQC5m6Rw2gsDXliMSmlZNbTpLRqyJPZ5agDjSUzBo7q2RkD7XOawgDGl +LmL4bq52AdgstsAHooOerkrsmfVxgzE1ep2SfP8Pl/3yPcwEUw/AUJJxusxWKiEx +Iz8+iP0AJqiUuDuTOkTZ7V7YVCbdmTRP+Vp3R4uVZEM1I+0+oyDvYg== +-----END RSA PRIVATE KEY----- diff --git a/pkg/blackfriday/LICENSE.txt b/pkg/blackfriday/LICENSE.txt new file mode 100644 index 00000000..2885af36 --- /dev/null +++ b/pkg/blackfriday/LICENSE.txt @@ -0,0 +1,29 @@ +Blackfriday is distributed under the Simplified BSD License: + +> Copyright © 2011 Russ Ross +> All rights reserved. +> +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions +> are met: +> +> 1. Redistributions of source code must retain the above copyright +> notice, this list of conditions and the following disclaimer. +> +> 2. Redistributions in binary form must reproduce the above +> copyright notice, this list of conditions and the following +> disclaimer in the documentation and/or other materials provided with +> the distribution. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +> POSSIBILITY OF SUCH DAMAGE. diff --git a/pkg/blackfriday/README.md b/pkg/blackfriday/README.md new file mode 100644 index 00000000..2e0db355 --- /dev/null +++ b/pkg/blackfriday/README.md @@ -0,0 +1,283 @@ +Blackfriday [![Build Status](https://travis-ci.org/russross/blackfriday.svg?branch=master)](https://travis-ci.org/russross/blackfriday) +=========== + +Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It +is paranoid about its input (so you can safely feed it user-supplied +data), it is fast, it supports common extensions (tables, smart +punctuation substitutions, etc.), and it is safe for all utf-8 +(unicode) input. + +HTML output is currently supported, along with Smartypants +extensions. + +It started as a translation from C of [Sundown][3]. + + +Installation +------------ + +Blackfriday is compatible with any modern Go release. With Go 1.7 and git +installed: + + go get gopkg.in/russross/blackfriday.v2 + +will download, compile, and install the package into your `$GOPATH` +directory hierarchy. Alternatively, you can achieve the same if you +import it into a project: + + import "gopkg.in/russross/blackfriday.v2" + +and `go get` without parameters. + + +Versions +-------- + +Currently maintained and recommended version of Blackfriday is `v2`. It's being +developed on its own branch: https://github.com/russross/blackfriday/v2. You +should install and import it via [gopkg.in][6] at +`gopkg.in/russross/blackfriday.v2`. + +Version 2 offers a number of improvements over v1: + +* Cleaned up API +* A separate call to [`Parse`][4], which produces an abstract syntax tree for + the document +* Latest bug fixes +* Flexibility to easily add your own rendering extensions + +Potential drawbacks: + +* Our benchmarks show v2 to be slightly slower than v1. Currently in the + ballpark of around 15%. +* API breakage. If you can't afford modifying your code to adhere to the new API + and don't care too much about the new features, v2 is probably not for you. +* Several bug fixes are trailing behind and still need to be forward-ported to + v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for + tracking. + +Usage +----- + +For the most sensible markdown processing, it is as simple as getting your input +into a byte slice and calling: + +```go +output := blackfriday.Run(input) +``` + +Your input will be parsed and the output rendered with a set of most popular +extensions enabled. If you want the most basic feature set, corresponding with +the bare Markdown specification, use: + +```go +output := blackfriday.Run(input, blackfriday.WithNoExtensions()) +``` + +### Sanitize untrusted content + +Blackfriday itself does nothing to protect against malicious content. If you are +dealing with user-supplied markdown, we recommend running Blackfriday's output +through HTML sanitizer such as [Bluemonday][5]. + +Here's an example of simple usage of Blackfriday together with Bluemonday: + +```go +import ( + "github.com/microcosm-cc/bluemonday" + "github.com/russross/blackfriday" +) + +// ... +unsafe := blackfriday.Run(input) +html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) +``` + +### Custom options + +If you want to customize the set of options, use `blackfriday.WithExtensions`, +`blackfriday.WithRenderer` and `blackfriday.WithRefOverride`. + +You can also check out `blackfriday-tool` for a more complete example +of how to use it. Download and install it using: + + go get github.com/russross/blackfriday-tool + +This is a simple command-line tool that allows you to process a +markdown file using a standalone program. You can also browse the +source directly on github if you are just looking for some example +code: + +* + +Note that if you have not already done so, installing +`blackfriday-tool` will be sufficient to download and install +blackfriday in addition to the tool itself. The tool binary will be +installed in `$GOPATH/bin`. This is a statically-linked binary that +can be copied to wherever you need it without worrying about +dependencies and library versions. + + +Features +-------- + +All features of Sundown are supported, including: + +* **Compatibility**. The Markdown v1.0.3 test suite passes with + the `--tidy` option. Without `--tidy`, the differences are + mostly in whitespace and entity escaping, where blackfriday is + more consistent and cleaner. + +* **Common extensions**, including table support, fenced code + blocks, autolinks, strikethroughs, non-strict emphasis, etc. + +* **Safety**. Blackfriday is paranoid when parsing, making it safe + to feed untrusted user input without fear of bad things + happening. The test suite stress tests this and there are no + known inputs that make it crash. If you find one, please let me + know and send me the input that does it. + + NOTE: "safety" in this context means *runtime safety only*. In order to + protect yourself against JavaScript injection in untrusted content, see + [this example](https://github.com/russross/blackfriday#sanitize-untrusted-content). + +* **Fast processing**. It is fast enough to render on-demand in + most web applications without having to cache the output. + +* **Thread safety**. You can run multiple parsers in different + goroutines without ill effect. There is no dependence on global + shared state. + +* **Minimal dependencies**. Blackfriday only depends on standard + library packages in Go. The source code is pretty + self-contained, so it is easy to add to any project, including + Google App Engine projects. + +* **Standards compliant**. Output successfully validates using the + W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional. + + +Extensions +---------- + +In addition to the standard markdown syntax, this package +implements the following extensions: + +* **Intra-word emphasis supression**. The `_` character is + commonly used inside words when discussing code, so having + markdown interpret it as an emphasis command is usually the + wrong thing. Blackfriday lets you treat all emphasis markers as + normal characters when they occur inside a word. + +* **Tables**. Tables can be created by drawing them in the input + using a simple syntax: + + ``` + Name | Age + --------|------ + Bob | 27 + Alice | 23 + ``` + +* **Fenced code blocks**. In addition to the normal 4-space + indentation to mark code blocks, you can explicitly mark them + and supply a language (to make syntax highlighting simple). Just + mark it like this: + + ```go + func getTrue() bool { + return true + } + ``` + + You can use 3 or more backticks to mark the beginning of the + block, and the same number to mark the end of the block. + +* **Definition lists**. A simple definition list is made of a single-line + term followed by a colon and the definition for that term. + + Cat + : Fluffy animal everyone likes + + Internet + : Vector of transmission for pictures of cats + + Terms must be separated from the previous definition by a blank line. + +* **Footnotes**. A marker in the text that will become a superscript number; + a footnote definition that will be placed in a list of footnotes at the + end of the document. A footnote looks like this: + + This is a footnote.[^1] + + [^1]: the footnote text. + +* **Autolinking**. Blackfriday can find URLs that have not been + explicitly marked as links and turn them into links. + +* **Strikethrough**. Use two tildes (`~~`) to mark text that + should be crossed out. + +* **Hard line breaks**. With this extension enabled newlines in the input + translate into line breaks in the output. This extension is off by default. + +* **Smart quotes**. Smartypants-style punctuation substitution is + supported, turning normal double- and single-quote marks into + curly quotes, etc. + +* **LaTeX-style dash parsing** is an additional option, where `--` + is translated into `–`, and `---` is translated into + `—`. This differs from most smartypants processors, which + turn a single hyphen into an ndash and a double hyphen into an + mdash. + +* **Smart fractions**, where anything that looks like a fraction + is translated into suitable HTML (instead of just a few special + cases like most smartypant processors). For example, `4/5` + becomes `45`, which renders as + 45. + + +Other renderers +--------------- + +Blackfriday is structured to allow alternative rendering engines. Here +are a few of note: + +* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown): + provides a GitHub Flavored Markdown renderer with fenced code block + highlighting, clickable heading anchor links. + + It's not customizable, and its goal is to produce HTML output + equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode), + except the rendering is performed locally. + +* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt, + but for markdown. + +* [LaTeX output](https://bitbucket.org/ambrevar/blackfriday-latex): + renders output as LaTeX. + + +Todo +---- + +* More unit testing +* Improve unicode support. It does not understand all unicode + rules (about what constitutes a letter, a punctuation symbol, + etc.), so it may fail to detect word boundaries correctly in + some instances. It is safe on all utf-8 input. + + +License +------- + +[Blackfriday is distributed under the Simplified BSD License](LICENSE.txt) + + + [1]: https://daringfireball.net/projects/markdown/ "Markdown" + [2]: https://golang.org/ "Go Language" + [3]: https://github.com/vmg/sundown "Sundown" + [4]: https://godoc.org/gopkg.in/russross/blackfriday.v2#Parse "Parse func" + [5]: https://github.com/microcosm-cc/bluemonday "Bluemonday" + [6]: https://labix.org/gopkg.in "gopkg.in" diff --git a/pkg/blackfriday/block.go b/pkg/blackfriday/block.go new file mode 100644 index 00000000..1f9c1a84 --- /dev/null +++ b/pkg/blackfriday/block.go @@ -0,0 +1,1558 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// Functions to parse block-level elements. +// + +package blackfriday + +import ( + "bytes" + "html" + "regexp" + + "github.com/shurcooL/sanitized_anchor_name" +) + +const ( + charEntity = "&(?:#x[a-f0-9]{1,8}|#[0-9]{1,8}|[a-z][a-z0-9]{1,31});" + escapable = "[!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]" +) + +var ( + reBackslashOrAmp = regexp.MustCompile("[\\&]") + reEntityOrEscapedChar = regexp.MustCompile("(?i)\\\\" + escapable + "|" + charEntity) +) + +// Parse block-level data. +// Note: this function and many that it calls assume that +// the input buffer ends with a newline. +func (p *Markdown) block(data []byte) { + // this is called recursively: enforce a maximum depth + if p.nesting >= p.maxNesting { + return + } + p.nesting++ + + // parse out one block-level construct at a time + for len(data) > 0 { + // prefixed heading: + // + // # Heading 1 + // ## Heading 2 + // ... + // ###### Heading 6 + if p.isPrefixHeading(data) { + data = data[p.prefixHeading(data):] + continue + } + + // block of preformatted HTML: + // + //
+ // ... + //
+ if data[0] == '<' { + if i := p.html(data, true); i > 0 { + data = data[i:] + continue + } + } + + // title block + // + // % stuff + // % more stuff + // % even more stuff + if p.extensions&Titleblock != 0 { + if data[0] == '%' { + if i := p.titleBlock(data, true); i > 0 { + data = data[i:] + continue + } + } + } + + // blank lines. note: returns the # of bytes to skip + if i := p.isEmpty(data); i > 0 { + data = data[i:] + continue + } + + // indented code block: + // + // func max(a, b int) int { + // if a > b { + // return a + // } + // return b + // } + if p.codePrefix(data) > 0 { + data = data[p.code(data):] + continue + } + + // fenced code block: + // + // ``` go + // func fact(n int) int { + // if n <= 1 { + // return n + // } + // return n * fact(n-1) + // } + // ``` + if p.extensions&FencedCode != 0 { + if i := p.fencedCodeBlock(data, true); i > 0 { + data = data[i:] + continue + } + } + + // horizontal rule: + // + // ------ + // or + // ****** + // or + // ______ + if p.isHRule(data) { + p.addBlock(HorizontalRule, nil) + var i int + for i = 0; i < len(data) && data[i] != '\n'; i++ { + } + data = data[i:] + continue + } + + // block quote: + // + // > A big quote I found somewhere + // > on the web + if p.quotePrefix(data) > 0 { + data = data[p.quote(data):] + continue + } + + // table: + // + // Name | Age | Phone + // ------|-----|--------- + // Bob | 31 | 555-1234 + // Alice | 27 | 555-4321 + if p.extensions&Tables != 0 { + if i := p.table(data); i > 0 { + data = data[i:] + continue + } + } + + // an itemized/unordered list: + // + // * Item 1 + // * Item 2 + // + // also works with + or - + if p.uliPrefix(data) > 0 { + data = data[p.list(data, 0):] + continue + } + + // a numbered/ordered list: + // + // 1. Item 1 + // 2. Item 2 + if p.oliPrefix(data) > 0 { + data = data[p.list(data, ListTypeOrdered):] + continue + } + + // definition lists: + // + // Term 1 + // : Definition a + // : Definition b + // + // Term 2 + // : Definition c + if p.extensions&DefinitionLists != 0 { + if p.dliPrefix(data) > 0 { + data = data[p.list(data, ListTypeDefinition):] + continue + } + } + + // anything else must look like a normal paragraph + // note: this finds underlined headings, too + data = data[p.paragraph(data):] + } + + p.nesting-- +} + +func (p *Markdown) addBlock(typ NodeType, content []byte) *Node { + p.closeUnmatchedBlocks() + container := p.addChild(typ, 0) + container.content = content + return container +} + +func (p *Markdown) isPrefixHeading(data []byte) bool { + if data[0] != '#' { + return false + } + + if p.extensions&SpaceHeadings != 0 { + level := 0 + for level < 6 && level < len(data) && data[level] == '#' { + level++ + } + if level == len(data) || data[level] != ' ' { + return false + } + } + return true +} + +func (p *Markdown) prefixHeading(data []byte) int { + level := 0 + for level < 6 && level < len(data) && data[level] == '#' { + level++ + } + i := skipChar(data, level, ' ') + end := skipUntilChar(data, i, '\n') + skip := end + id := "" + if p.extensions&HeadingIDs != 0 { + j, k := 0, 0 + // find start/end of heading id + for j = i; j < end-1 && (data[j] != '{' || data[j+1] != '#'); j++ { + } + for k = j + 1; k < end && data[k] != '}'; k++ { + } + // extract heading id iff found + if j < end && k < end { + id = string(data[j+2 : k]) + end = j + skip = k + 1 + for end > 0 && data[end-1] == ' ' { + end-- + } + } + } + for end > 0 && data[end-1] == '#' { + if isBackslashEscaped(data, end-1) { + break + } + end-- + } + for end > 0 && data[end-1] == ' ' { + end-- + } + if end > i { + if id == "" && p.extensions&AutoHeadingIDs != 0 { + id = sanitized_anchor_name.Create(string(data[i:end])) + } + block := p.addBlock(Heading, data[i:end]) + block.HeadingID = id + block.Level = level + } + return skip +} + +func (p *Markdown) isUnderlinedHeading(data []byte) int { + // test of level 1 heading + if data[0] == '=' { + i := skipChar(data, 1, '=') + i = skipChar(data, i, ' ') + if i < len(data) && data[i] == '\n' { + return 1 + } + return 0 + } + + // test of level 2 heading + if data[0] == '-' { + i := skipChar(data, 1, '-') + i = skipChar(data, i, ' ') + if i < len(data) && data[i] == '\n' { + return 2 + } + return 0 + } + + return 0 +} + +func (p *Markdown) titleBlock(data []byte, doRender bool) int { + if data[0] != '%' { + return 0 + } + splitData := bytes.Split(data, []byte("\n")) + var i int + for idx, b := range splitData { + if !bytes.HasPrefix(b, []byte("%")) { + i = idx // - 1 + break + } + } + + data = bytes.Join(splitData[0:i], []byte("\n")) + consumed := len(data) + data = bytes.TrimPrefix(data, []byte("% ")) + data = bytes.Replace(data, []byte("\n% "), []byte("\n"), -1) + block := p.addBlock(Heading, data) + block.Level = 1 + block.IsTitleblock = true + + return consumed +} + +func (p *Markdown) html(data []byte, doRender bool) int { + var i, j int + + // identify the opening tag + if data[0] != '<' { + return 0 + } + curtag, tagfound := p.htmlFindTag(data[1:]) + + // handle special cases + if !tagfound { + // check for an HTML comment + if size := p.htmlComment(data, doRender); size > 0 { + return size + } + + // check for an
tag + if size := p.htmlHr(data, doRender); size > 0 { + return size + } + + // no special case recognized + return 0 + } + + // look for an unindented matching closing tag + // followed by a blank line + found := false + /* + closetag := []byte("\n") + j = len(curtag) + 1 + for !found { + // scan for a closing tag at the beginning of a line + if skip := bytes.Index(data[j:], closetag); skip >= 0 { + j += skip + len(closetag) + } else { + break + } + + // see if it is the only thing on the line + if skip := p.isEmpty(data[j:]); skip > 0 { + // see if it is followed by a blank line/eof + j += skip + if j >= len(data) { + found = true + i = j + } else { + if skip := p.isEmpty(data[j:]); skip > 0 { + j += skip + found = true + i = j + } + } + } + } + */ + + // if not found, try a second pass looking for indented match + // but not if tag is "ins" or "del" (following original Markdown.pl) + if !found && curtag != "ins" && curtag != "del" { + i = 1 + for i < len(data) { + i++ + for i < len(data) && !(data[i-1] == '<' && data[i] == '/') { + i++ + } + + if i+2+len(curtag) >= len(data) { + break + } + + j = p.htmlFindEnd(curtag, data[i-1:]) + + if j > 0 { + i += j - 1 + found = true + break + } + } + } + + if !found { + return 0 + } + + // the end of the block has been found + if doRender { + // trim newlines + end := i + for end > 0 && data[end-1] == '\n' { + end-- + } + finalizeHTMLBlock(p.addBlock(HTMLBlock, data[:end])) + } + + return i +} + +func finalizeHTMLBlock(block *Node) { + block.Literal = block.content + block.content = nil +} + +// HTML comment, lax form +func (p *Markdown) htmlComment(data []byte, doRender bool) int { + i := p.inlineHTMLComment(data) + // needs to end with a blank line + if j := p.isEmpty(data[i:]); j > 0 { + size := i + j + if doRender { + // trim trailing newlines + end := size + for end > 0 && data[end-1] == '\n' { + end-- + } + block := p.addBlock(HTMLBlock, data[:end]) + finalizeHTMLBlock(block) + } + return size + } + return 0 +} + +// HR, which is the only self-closing block tag considered +func (p *Markdown) htmlHr(data []byte, doRender bool) int { + if len(data) < 4 { + return 0 + } + if data[0] != '<' || (data[1] != 'h' && data[1] != 'H') || (data[2] != 'r' && data[2] != 'R') { + return 0 + } + if data[3] != ' ' && data[3] != '/' && data[3] != '>' { + // not an
tag after all; at least not a valid one + return 0 + } + i := 3 + for i < len(data) && data[i] != '>' && data[i] != '\n' { + i++ + } + if i < len(data) && data[i] == '>' { + i++ + if j := p.isEmpty(data[i:]); j > 0 { + size := i + j + if doRender { + // trim newlines + end := size + for end > 0 && data[end-1] == '\n' { + end-- + } + finalizeHTMLBlock(p.addBlock(HTMLBlock, data[:end])) + } + return size + } + } + return 0 +} + +func (p *Markdown) htmlFindTag(data []byte) (string, bool) { + i := 0 + for i < len(data) && isalnum(data[i]) { + i++ + } + key := string(data[:i]) + if _, ok := blockTags[key]; ok { + return key, true + } + return "", false +} + +func (p *Markdown) htmlFindEnd(tag string, data []byte) int { + // assume data[0] == '<' && data[1] == '/' already tested + if tag == "hr" { + return 2 + } + // check if tag is a match + closetag := []byte("") + if !bytes.HasPrefix(data, closetag) { + return 0 + } + i := len(closetag) + + // check that the rest of the line is blank + skip := 0 + if skip = p.isEmpty(data[i:]); skip == 0 { + return 0 + } + i += skip + skip = 0 + + if i >= len(data) { + return i + } + + if p.extensions&LaxHTMLBlocks != 0 { + return i + } + if skip = p.isEmpty(data[i:]); skip == 0 { + // following line must be blank + return 0 + } + + return i + skip +} + +func (*Markdown) isEmpty(data []byte) int { + // it is okay to call isEmpty on an empty buffer + if len(data) == 0 { + return 0 + } + + var i int + for i = 0; i < len(data) && data[i] != '\n'; i++ { + if data[i] != ' ' && data[i] != '\t' { + return 0 + } + } + if i < len(data) && data[i] == '\n' { + i++ + } + return i +} + +func (*Markdown) isHRule(data []byte) bool { + i := 0 + + // skip up to three spaces + for i < 3 && data[i] == ' ' { + i++ + } + + // look at the hrule char + if data[i] != '*' && data[i] != '-' && data[i] != '_' { + return false + } + c := data[i] + + // the whole line must be the char or whitespace + n := 0 + for i < len(data) && data[i] != '\n' { + switch { + case data[i] == c: + n++ + case data[i] != ' ': + return false + } + i++ + } + + return n >= 3 +} + +// isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data, +// and returns the end index if so, or 0 otherwise. It also returns the marker found. +// If syntax is not nil, it gets set to the syntax specified in the fence line. +func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker string) { + i, size := 0, 0 + + // skip up to three spaces + for i < len(data) && i < 3 && data[i] == ' ' { + i++ + } + + // check for the marker characters: ~ or ` + if i >= len(data) { + return 0, "" + } + if data[i] != '~' && data[i] != '`' { + return 0, "" + } + + c := data[i] + + // the whole line must be the same char or whitespace + for i < len(data) && data[i] == c { + size++ + i++ + } + + // the marker char must occur at least 3 times + if size < 3 { + return 0, "" + } + marker = string(data[i-size : i]) + + // if this is the end marker, it must match the beginning marker + if oldmarker != "" && marker != oldmarker { + return 0, "" + } + + // TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here + // into one, always get the syntax, and discard it if the caller doesn't care. + if syntax != nil { + syn := 0 + i = skipChar(data, i, ' ') + + if i >= len(data) { + if i == len(data) { + return i, marker + } + return 0, "" + } + + syntaxStart := i + + if data[i] == '{' { + i++ + syntaxStart++ + + for i < len(data) && data[i] != '}' && data[i] != '\n' { + syn++ + i++ + } + + if i >= len(data) || data[i] != '}' { + return 0, "" + } + + // strip all whitespace at the beginning and the end + // of the {} block + for syn > 0 && isspace(data[syntaxStart]) { + syntaxStart++ + syn-- + } + + for syn > 0 && isspace(data[syntaxStart+syn-1]) { + syn-- + } + + i++ + } else { + for i < len(data) && !isspace(data[i]) { + syn++ + i++ + } + } + + *syntax = string(data[syntaxStart : syntaxStart+syn]) + } + + i = skipChar(data, i, ' ') + if i >= len(data) || data[i] != '\n' { + if i == len(data) { + return i, marker + } + return 0, "" + } + return i + 1, marker // Take newline into account. +} + +// fencedCodeBlock returns the end index if data contains a fenced code block at the beginning, +// or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects. +// If doRender is true, a final newline is mandatory to recognize the fenced code block. +func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int { + var syntax string + beg, marker := isFenceLine(data, &syntax, "") + if beg == 0 || beg >= len(data) { + return 0 + } + + var work bytes.Buffer + work.Write([]byte(syntax)) + work.WriteByte('\n') + + for { + // safe to assume beg < len(data) + + // check for the end of the code block + fenceEnd, _ := isFenceLine(data[beg:], nil, marker) + if fenceEnd != 0 { + beg += fenceEnd + break + } + + // copy the current line + end := skipUntilChar(data, beg, '\n') + 1 + + // did we reach the end of the buffer without a closing marker? + if end >= len(data) { + return 0 + } + + // verbatim copy to the working buffer + if doRender { + work.Write(data[beg:end]) + } + beg = end + } + + if doRender { + block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer + block.IsFenced = true + finalizeCodeBlock(block) + } + + return beg +} + +func unescapeChar(str []byte) []byte { + if str[0] == '\\' { + return []byte{str[1]} + } + return []byte(html.UnescapeString(string(str))) +} + +func unescapeString(str []byte) []byte { + if reBackslashOrAmp.Match(str) { + return reEntityOrEscapedChar.ReplaceAllFunc(str, unescapeChar) + } + return str +} + +func finalizeCodeBlock(block *Node) { + if block.IsFenced { + newlinePos := bytes.IndexByte(block.content, '\n') + firstLine := block.content[:newlinePos] + rest := block.content[newlinePos+1:] + block.Info = unescapeString(bytes.Trim(firstLine, "\n")) + block.Literal = rest + } else { + block.Literal = block.content + } + block.content = nil +} + +func (p *Markdown) table(data []byte) int { + table := p.addBlock(Table, nil) + i, columns := p.tableHeader(data) + if i == 0 { + p.tip = table.Parent + table.Unlink() + return 0 + } + + p.addBlock(TableBody, nil) + + for i < len(data) { + pipes, rowStart := 0, i + for ; i < len(data) && data[i] != '\n'; i++ { + if data[i] == '|' { + pipes++ + } + } + + if pipes == 0 { + i = rowStart + break + } + + // include the newline in data sent to tableRow + if i < len(data) && data[i] == '\n' { + i++ + } + p.tableRow(data[rowStart:i], columns, false) + } + + return i +} + +// check if the specified position is preceded by an odd number of backslashes +func isBackslashEscaped(data []byte, i int) bool { + backslashes := 0 + for i-backslashes-1 >= 0 && data[i-backslashes-1] == '\\' { + backslashes++ + } + return backslashes&1 == 1 +} + +func (p *Markdown) tableHeader(data []byte) (size int, columns []CellAlignFlags) { + i := 0 + colCount := 1 + for i = 0; i < len(data) && data[i] != '\n'; i++ { + if data[i] == '|' && !isBackslashEscaped(data, i) { + colCount++ + } + } + + // doesn't look like a table header + if colCount == 1 { + return + } + + // include the newline in the data sent to tableRow + j := i + if j < len(data) && data[j] == '\n' { + j++ + } + header := data[:j] + + // column count ignores pipes at beginning or end of line + if data[0] == '|' { + colCount-- + } + if i > 2 && data[i-1] == '|' && !isBackslashEscaped(data, i-1) { + colCount-- + } + + columns = make([]CellAlignFlags, colCount) + + // move on to the header underline + i++ + if i >= len(data) { + return + } + + if data[i] == '|' && !isBackslashEscaped(data, i) { + i++ + } + i = skipChar(data, i, ' ') + + // each column header is of form: / *:?-+:? *|/ with # dashes + # colons >= 3 + // and trailing | optional on last column + col := 0 + for i < len(data) && data[i] != '\n' { + dashes := 0 + + if data[i] == ':' { + i++ + columns[col] |= TableAlignmentLeft + dashes++ + } + for i < len(data) && data[i] == '-' { + i++ + dashes++ + } + if i < len(data) && data[i] == ':' { + i++ + columns[col] |= TableAlignmentRight + dashes++ + } + for i < len(data) && data[i] == ' ' { + i++ + } + if i == len(data) { + return + } + // end of column test is messy + switch { + case dashes < 3: + // not a valid column + return + + case data[i] == '|' && !isBackslashEscaped(data, i): + // marker found, now skip past trailing whitespace + col++ + i++ + for i < len(data) && data[i] == ' ' { + i++ + } + + // trailing junk found after last column + if col >= colCount && i < len(data) && data[i] != '\n' { + return + } + + case (data[i] != '|' || isBackslashEscaped(data, i)) && col+1 < colCount: + // something else found where marker was required + return + + case data[i] == '\n': + // marker is optional for the last column + col++ + + default: + // trailing junk found after last column + return + } + } + if col != colCount { + return + } + + p.addBlock(TableHead, nil) + p.tableRow(header, columns, true) + size = i + if size < len(data) && data[size] == '\n' { + size++ + } + return +} + +func (p *Markdown) tableRow(data []byte, columns []CellAlignFlags, header bool) { + p.addBlock(TableRow, nil) + i, col := 0, 0 + + if data[i] == '|' && !isBackslashEscaped(data, i) { + i++ + } + + for col = 0; col < len(columns) && i < len(data); col++ { + for i < len(data) && data[i] == ' ' { + i++ + } + + cellStart := i + + for i < len(data) && (data[i] != '|' || isBackslashEscaped(data, i)) && data[i] != '\n' { + i++ + } + + cellEnd := i + + // skip the end-of-cell marker, possibly taking us past end of buffer + i++ + + for cellEnd > cellStart && cellEnd-1 < len(data) && data[cellEnd-1] == ' ' { + cellEnd-- + } + + cell := p.addBlock(TableCell, data[cellStart:cellEnd]) + cell.IsHeader = header + cell.Align = columns[col] + } + + // pad it out with empty columns to get the right number + for ; col < len(columns); col++ { + cell := p.addBlock(TableCell, nil) + cell.IsHeader = header + cell.Align = columns[col] + } + + // silently ignore rows with too many cells +} + +// returns blockquote prefix length +func (p *Markdown) quotePrefix(data []byte) int { + i := 0 + for i < 3 && i < len(data) && data[i] == ' ' { + i++ + } + if i < len(data) && data[i] == '>' { + if i+1 < len(data) && data[i+1] == ' ' { + return i + 2 + } + return i + 1 + } + return 0 +} + +// blockquote ends with at least one blank line +// followed by something without a blockquote prefix +func (p *Markdown) terminateBlockquote(data []byte, beg, end int) bool { + if p.isEmpty(data[beg:]) <= 0 { + return false + } + if end >= len(data) { + return true + } + return p.quotePrefix(data[end:]) == 0 && p.isEmpty(data[end:]) == 0 +} + +// parse a blockquote fragment +func (p *Markdown) quote(data []byte) int { + block := p.addBlock(BlockQuote, nil) + var raw bytes.Buffer + beg, end := 0, 0 + for beg < len(data) { + end = beg + // Step over whole lines, collecting them. While doing that, check for + // fenced code and if one's found, incorporate it altogether, + // irregardless of any contents inside it + for end < len(data) && data[end] != '\n' { + if p.extensions&FencedCode != 0 { + if i := p.fencedCodeBlock(data[end:], false); i > 0 { + // -1 to compensate for the extra end++ after the loop: + end += i - 1 + break + } + } + end++ + } + if end < len(data) && data[end] == '\n' { + end++ + } + if pre := p.quotePrefix(data[beg:]); pre > 0 { + // skip the prefix + beg += pre + } else if p.terminateBlockquote(data, beg, end) { + break + } + // this line is part of the blockquote + raw.Write(data[beg:end]) + beg = end + } + p.block(raw.Bytes()) + p.finalize(block) + return end +} + +// returns prefix length for block code +func (p *Markdown) codePrefix(data []byte) int { + if len(data) >= 1 && data[0] == '\t' { + return 1 + } + if len(data) >= 4 && data[0] == ' ' && data[1] == ' ' && data[2] == ' ' && data[3] == ' ' { + return 4 + } + return 0 +} + +func (p *Markdown) code(data []byte) int { + var work bytes.Buffer + + i := 0 + for i < len(data) { + beg := i + for i < len(data) && data[i] != '\n' { + i++ + } + if i < len(data) && data[i] == '\n' { + i++ + } + + blankline := p.isEmpty(data[beg:i]) > 0 + if pre := p.codePrefix(data[beg:i]); pre > 0 { + beg += pre + } else if !blankline { + // non-empty, non-prefixed line breaks the pre + i = beg + break + } + + // verbatim copy to the working buffer + if blankline { + work.WriteByte('\n') + } else { + work.Write(data[beg:i]) + } + } + + // trim all the \n off the end of work + workbytes := work.Bytes() + eol := len(workbytes) + for eol > 0 && workbytes[eol-1] == '\n' { + eol-- + } + if eol != len(workbytes) { + work.Truncate(eol) + } + + work.WriteByte('\n') + + block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer + block.IsFenced = false + finalizeCodeBlock(block) + + return i +} + +// returns unordered list item prefix +func (p *Markdown) uliPrefix(data []byte) int { + i := 0 + // start with up to 3 spaces + for i < len(data) && i < 3 && data[i] == ' ' { + i++ + } + if i >= len(data)-1 { + return 0 + } + // need one of {'*', '+', '-'} followed by a space or a tab + if (data[i] != '*' && data[i] != '+' && data[i] != '-') || + (data[i+1] != ' ' && data[i+1] != '\t') { + return 0 + } + return i + 2 +} + +// returns ordered list item prefix +func (p *Markdown) oliPrefix(data []byte) int { + i := 0 + + // start with up to 3 spaces + for i < 3 && i < len(data) && data[i] == ' ' { + i++ + } + + // count the digits + start := i + for i < len(data) && data[i] >= '0' && data[i] <= '9' { + i++ + } + if start == i || i >= len(data)-1 { + return 0 + } + + // we need >= 1 digits followed by a dot and a space or a tab + if data[i] != '.' || !(data[i+1] == ' ' || data[i+1] == '\t') { + return 0 + } + return i + 2 +} + +// returns definition list item prefix +func (p *Markdown) dliPrefix(data []byte) int { + if len(data) < 2 { + return 0 + } + i := 0 + // need a ':' followed by a space or a tab + if data[i] != ':' || !(data[i+1] == ' ' || data[i+1] == '\t') { + return 0 + } + for i < len(data) && data[i] == ' ' { + i++ + } + return i + 2 +} + +// parse ordered or unordered list block +func (p *Markdown) list(data []byte, flags ListType) int { + i := 0 + flags |= ListItemBeginningOfList + block := p.addBlock(List, nil) + block.ListFlags = flags + block.Tight = true + + for i < len(data) { + skip := p.listItem(data[i:], &flags) + if flags&ListItemContainsBlock != 0 { + block.ListData.Tight = false + } + i += skip + if skip == 0 || flags&ListItemEndOfList != 0 { + break + } + flags &= ^ListItemBeginningOfList + } + + above := block.Parent + finalizeList(block) + p.tip = above + return i +} + +// Returns true if block ends with a blank line, descending if needed +// into lists and sublists. +func endsWithBlankLine(block *Node) bool { + // TODO: figure this out. Always false now. + for block != nil { + //if block.lastLineBlank { + //return true + //} + t := block.Type + if t == List || t == Item { + block = block.LastChild + } else { + break + } + } + return false +} + +func finalizeList(block *Node) { + block.open = false + item := block.FirstChild + for item != nil { + // check for non-final list item ending with blank line: + if endsWithBlankLine(item) && item.Next != nil { + block.ListData.Tight = false + break + } + // recurse into children of list item, to see if there are spaces + // between any of them: + subItem := item.FirstChild + for subItem != nil { + if endsWithBlankLine(subItem) && (item.Next != nil || subItem.Next != nil) { + block.ListData.Tight = false + break + } + subItem = subItem.Next + } + item = item.Next + } +} + +// Parse a single list item. +// Assumes initial prefix is already removed if this is a sublist. +func (p *Markdown) listItem(data []byte, flags *ListType) int { + // keep track of the indentation of the first line + itemIndent := 0 + if data[0] == '\t' { + itemIndent += 4 + } else { + for itemIndent < 3 && data[itemIndent] == ' ' { + itemIndent++ + } + } + + var bulletChar byte = '*' + i := p.uliPrefix(data) + if i == 0 { + i = p.oliPrefix(data) + } else { + bulletChar = data[i-2] + } + if i == 0 { + i = p.dliPrefix(data) + // reset definition term flag + if i > 0 { + *flags &= ^ListTypeTerm + } + } + if i == 0 { + // if in definition list, set term flag and continue + if *flags&ListTypeDefinition != 0 { + *flags |= ListTypeTerm + } else { + return 0 + } + } + + // skip leading whitespace on first line + for i < len(data) && data[i] == ' ' { + i++ + } + + // find the end of the line + line := i + for i > 0 && i < len(data) && data[i-1] != '\n' { + i++ + } + + // get working buffer + var raw bytes.Buffer + + // put the first line into the working buffer + raw.Write(data[line:i]) + line = i + + // process the following lines + containsBlankLine := false + sublist := 0 + lastChunkSize := 0 + +gatherlines: + for line < len(data) { + i++ + + // find the end of this line + for i < len(data) && data[i-1] != '\n' { + i++ + } + + // if it is an empty line, guess that it is part of this item + // and move on to the next line + if p.isEmpty(data[line:i]) > 0 { + containsBlankLine = true + line = i + continue + } + + // calculate the indentation + indent := 0 + indentIndex := 0 + if data[line] == '\t' { + indentIndex++ + indent += 4 + } else { + for indent < 4 && line+indent < i && data[line+indent] == ' ' { + indent++ + indentIndex++ + } + } + + chunk := data[line+indentIndex : i] + + // evaluate how this line fits in + switch { + // is this a nested list item? + case (p.uliPrefix(chunk) > 0 && !p.isHRule(chunk)) || + p.oliPrefix(chunk) > 0 || + p.dliPrefix(chunk) > 0: + + if containsBlankLine { + *flags |= ListItemContainsBlock + } + + // to be a nested list, it must be indented more + // if not, it is the next item in the same list + if indent <= itemIndent { + break gatherlines + } + + // is this the first item in the nested list? + if sublist == 0 { + if p.dliPrefix(chunk) > 0 { + sublist = raw.Len() - lastChunkSize + } else { + sublist = raw.Len() + } + } + + // is this a nested prefix heading? + case p.isPrefixHeading(chunk): + // if the heading is not indented, it is not nested in the list + // and thus ends the list + if containsBlankLine && indent < 4 { + *flags |= ListItemEndOfList + break gatherlines + } + *flags |= ListItemContainsBlock + + // anything following an empty line is only part + // of this item if it is indented 4 spaces + // (regardless of the indentation of the beginning of the item) + case containsBlankLine && indent < 4: + if *flags&ListTypeDefinition != 0 && i < len(data)-1 { + // is the next item still a part of this list? + next := i + for next < len(data) && data[next] != '\n' { + next++ + } + for next < len(data)-1 && data[next] == '\n' { + next++ + } + if i < len(data)-1 && data[i] != ':' && data[next] != ':' { + *flags |= ListItemEndOfList + } + } else { + *flags |= ListItemEndOfList + } + break gatherlines + + // a blank line means this should be parsed as a block + case containsBlankLine: + raw.WriteByte('\n') + *flags |= ListItemContainsBlock + } + + // if this line was preceded by one or more blanks, + // re-introduce the blank into the buffer + if containsBlankLine { + containsBlankLine = false + raw.WriteByte('\n') + } + + // add the line into the working buffer without prefix + raw.Write(data[line+indentIndex : i]) + + // remember how much was written into raw, if this turns out to be a + // definition list we'll need this number to know where the sublist starts + lastChunkSize = i - (line + indentIndex) + + line = i + } + + rawBytes := raw.Bytes() + + block := p.addBlock(Item, nil) + block.ListFlags = *flags + block.Tight = false + block.BulletChar = bulletChar + block.Delimiter = '.' // Only '.' is possible in Markdown, but ')' will also be possible in CommonMark + + // render the contents of the list item + if *flags&ListItemContainsBlock != 0 && *flags&ListTypeTerm == 0 { + // intermediate render of block item, except for definition term + if sublist > 0 { + p.block(rawBytes[:sublist]) + p.block(rawBytes[sublist:]) + } else { + p.block(rawBytes) + } + } else { + // intermediate render of inline item + if sublist > 0 { + child := p.addChild(Paragraph, 0) + child.content = rawBytes[:sublist] + p.block(rawBytes[sublist:]) + } else { + child := p.addChild(Paragraph, 0) + child.content = rawBytes + } + } + return line +} + +// render a single paragraph that has already been parsed out +func (p *Markdown) renderParagraph(data []byte) { + if len(data) == 0 { + return + } + + // trim leading spaces + beg := 0 + for data[beg] == ' ' { + beg++ + } + + end := len(data) + // trim trailing newline + if data[len(data)-1] == '\n' { + end-- + } + + // trim trailing spaces + for end > beg && data[end-1] == ' ' { + end-- + } + + p.addBlock(Paragraph, data[beg:end]) +} + +func (p *Markdown) paragraph(data []byte) int { + // prev: index of 1st char of previous line + // line: index of 1st char of current line + // i: index of cursor/end of current line + var prev, line, i int + tabSize := TabSizeDefault + if p.extensions&TabSizeEight != 0 { + tabSize = TabSizeDouble + } + // keep going until we find something to mark the end of the paragraph + for i < len(data) { + // mark the beginning of the current line + prev = line + current := data[i:] + line = i + + // did we find a reference or a footnote? If so, end a paragraph + // preceding it and report that we have consumed up to the end of that + // reference: + if refEnd := isReference(p, current, tabSize); refEnd > 0 { + p.renderParagraph(data[:i]) + return i + refEnd + } + + // did we find a blank line marking the end of the paragraph? + if n := p.isEmpty(current); n > 0 { + // did this blank line followed by a definition list item? + if p.extensions&DefinitionLists != 0 { + if i < len(data)-1 && data[i+1] == ':' { + return p.list(data[prev:], ListTypeDefinition) + } + } + + p.renderParagraph(data[:i]) + return i + n + } + + // an underline under some text marks a heading, so our paragraph ended on prev line + if i > 0 { + if level := p.isUnderlinedHeading(current); level > 0 { + // render the paragraph + p.renderParagraph(data[:prev]) + + // ignore leading and trailing whitespace + eol := i - 1 + for prev < eol && data[prev] == ' ' { + prev++ + } + for eol > prev && data[eol-1] == ' ' { + eol-- + } + + id := "" + if p.extensions&AutoHeadingIDs != 0 { + id = sanitized_anchor_name.Create(string(data[prev:eol])) + } + + block := p.addBlock(Heading, data[prev:eol]) + block.Level = level + block.HeadingID = id + + // find the end of the underline + for i < len(data) && data[i] != '\n' { + i++ + } + return i + } + } + + // if the next line starts a block of HTML, then the paragraph ends here + if p.extensions&LaxHTMLBlocks != 0 { + if data[i] == '<' && p.html(current, false) > 0 { + // rewind to before the HTML block + p.renderParagraph(data[:i]) + return i + } + } + + // if there's a prefixed heading or a horizontal rule after this, paragraph is over + if p.isPrefixHeading(current) || p.isHRule(current) { + p.renderParagraph(data[:i]) + return i + } + + // if there's a fenced code block, paragraph is over + if p.extensions&FencedCode != 0 { + if p.fencedCodeBlock(current, false) > 0 { + p.renderParagraph(data[:i]) + return i + } + } + + // if there's a definition list item, prev line is a definition term + if p.extensions&DefinitionLists != 0 { + if p.dliPrefix(current) != 0 { + ret := p.list(data[prev:], ListTypeDefinition) + return ret + } + } + + // if there's a list after this, paragraph is over + if p.extensions&NoEmptyLineBeforeBlock != 0 { + if p.uliPrefix(current) != 0 || + p.oliPrefix(current) != 0 || + p.quotePrefix(current) != 0 || + p.codePrefix(current) != 0 { + p.renderParagraph(data[:i]) + return i + } + } + + // otherwise, scan to the beginning of the next line + nl := bytes.IndexByte(data[i:], '\n') + if nl >= 0 { + i += nl + 1 + } else { + i += len(data[i:]) + } + } + + p.renderParagraph(data[:i]) + return i +} + +func skipChar(data []byte, start int, char byte) int { + i := start + for i < len(data) && data[i] == char { + i++ + } + return i +} + +func skipUntilChar(text []byte, start int, char byte) int { + i := start + for i < len(text) && text[i] != char { + i++ + } + return i +} diff --git a/pkg/blackfriday/block_test.go b/pkg/blackfriday/block_test.go new file mode 100644 index 00000000..0a2a4d84 --- /dev/null +++ b/pkg/blackfriday/block_test.go @@ -0,0 +1,1691 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// Unit tests for block parsing +// + +package blackfriday + +import ( + "strings" + "testing" +) + +func TestPrefixHeaderNoExtensions(t *testing.T) { + var tests = []string{ + "# Header 1\n", + "

Header 1

\n", + + "## Header 2\n", + "

Header 2

\n", + + "### Header 3\n", + "

Header 3

\n", + + "#### Header 4\n", + "

Header 4

\n", + + "##### Header 5\n", + "
Header 5
\n", + + "###### Header 6\n", + "
Header 6
\n", + + "####### Header 7\n", + "
# Header 7
\n", + + "#Header 1\n", + "

Header 1

\n", + + "##Header 2\n", + "

Header 2

\n", + + "###Header 3\n", + "

Header 3

\n", + + "####Header 4\n", + "

Header 4

\n", + + "#####Header 5\n", + "
Header 5
\n", + + "######Header 6\n", + "
Header 6
\n", + + "#######Header 7\n", + "
#Header 7
\n", + + "Hello\n# Header 1\nGoodbye\n", + "

Hello

\n\n

Header 1

\n\n

Goodbye

\n", + + "* List\n# Header\n* List\n", + "
    \n
  • List

    \n\n

    Header

  • \n\n
  • List

  • \n
\n", + + "* List\n#Header\n* List\n", + "
    \n
  • List

    \n\n

    Header

  • \n\n
  • List

  • \n
\n", + + "* List\n * Nested list\n # Nested header\n", + "
    \n
  • List

    \n\n
      \n
    • Nested list

      \n\n" + + "

      Nested header

    • \n
  • \n
\n", + + "#Header 1 \\#\n", + "

Header 1 #

\n", + + "#Header 1 \\# foo\n", + "

Header 1 # foo

\n", + + "#Header 1 #\\##\n", + "

Header 1 ##

\n", + } + doTestsBlock(t, tests, 0) +} + +func TestPrefixHeaderSpaceExtension(t *testing.T) { + var tests = []string{ + "# Header 1\n", + "

Header 1

\n", + + "## Header 2\n", + "

Header 2

\n", + + "### Header 3\n", + "

Header 3

\n", + + "#### Header 4\n", + "

Header 4

\n", + + "##### Header 5\n", + "
Header 5
\n", + + "###### Header 6\n", + "
Header 6
\n", + + "####### Header 7\n", + "

####### Header 7

\n", + + "#Header 1\n", + "

#Header 1

\n", + + "##Header 2\n", + "

##Header 2

\n", + + "###Header 3\n", + "

###Header 3

\n", + + "####Header 4\n", + "

####Header 4

\n", + + "#####Header 5\n", + "

#####Header 5

\n", + + "######Header 6\n", + "

######Header 6

\n", + + "#######Header 7\n", + "

#######Header 7

\n", + + "Hello\n# Header 1\nGoodbye\n", + "

Hello

\n\n

Header 1

\n\n

Goodbye

\n", + + "* List\n# Header\n* List\n", + "
    \n
  • List

    \n\n

    Header

  • \n\n
  • List

  • \n
\n", + + "* List\n#Header\n* List\n", + "
    \n
  • List\n#Header
  • \n
  • List
  • \n
\n", + + "* List\n * Nested list\n # Nested header\n", + "
    \n
  • List

    \n\n
      \n
    • Nested list

      \n\n" + + "

      Nested header

    • \n
  • \n
\n", + } + doTestsBlock(t, tests, SpaceHeadings) +} + +func TestPrefixHeaderIdExtension(t *testing.T) { + var tests = []string{ + "# Header 1 {#someid}\n", + "

Header 1

\n", + + "# Header 1 {#someid} \n", + "

Header 1

\n", + + "# Header 1 {#someid}\n", + "

Header 1

\n", + + "# Header 1 {#someid\n", + "

Header 1 {#someid

\n", + + "# Header 1 {#someid\n", + "

Header 1 {#someid

\n", + + "# Header 1 {#someid}}\n", + "

Header 1

\n\n

}

\n", + + "## Header 2 {#someid}\n", + "

Header 2

\n", + + "### Header 3 {#someid}\n", + "

Header 3

\n", + + "#### Header 4 {#someid}\n", + "

Header 4

\n", + + "##### Header 5 {#someid}\n", + "
Header 5
\n", + + "###### Header 6 {#someid}\n", + "
Header 6
\n", + + "####### Header 7 {#someid}\n", + "
# Header 7
\n", + + "# Header 1 # {#someid}\n", + "

Header 1

\n", + + "## Header 2 ## {#someid}\n", + "

Header 2

\n", + + "Hello\n# Header 1\nGoodbye\n", + "

Hello

\n\n

Header 1

\n\n

Goodbye

\n", + + "* List\n# Header {#someid}\n* List\n", + "
    \n
  • List

    \n\n

    Header

  • \n\n
  • List

  • \n
\n", + + "* List\n#Header {#someid}\n* List\n", + "
    \n
  • List

    \n\n

    Header

  • \n\n
  • List

  • \n
\n", + + "* List\n * Nested list\n # Nested header {#someid}\n", + "
    \n
  • List

    \n\n
      \n
    • Nested list

      \n\n" + + "

      Nested header

    • \n
  • \n
\n", + } + doTestsBlock(t, tests, HeadingIDs) +} + +func TestPrefixHeaderIdExtensionWithPrefixAndSuffix(t *testing.T) { + var tests = []string{ + "# header 1 {#someid}\n", + "

header 1

\n", + + "## header 2 {#someid}\n", + "

header 2

\n", + + "### header 3 {#someid}\n", + "

header 3

\n", + + "#### header 4 {#someid}\n", + "

header 4

\n", + + "##### header 5 {#someid}\n", + "
header 5
\n", + + "###### header 6 {#someid}\n", + "
header 6
\n", + + "####### header 7 {#someid}\n", + "
# header 7
\n", + + "# header 1 # {#someid}\n", + "

header 1

\n", + + "## header 2 ## {#someid}\n", + "

header 2

\n", + + "* List\n# Header {#someid}\n* List\n", + "
    \n
  • List

    \n\n

    Header

  • \n\n
  • List

  • \n
\n", + + "* List\n#Header {#someid}\n* List\n", + "
    \n
  • List

    \n\n

    Header

  • \n\n
  • List

  • \n
\n", + + "* List\n * Nested list\n # Nested header {#someid}\n", + "
    \n
  • List

    \n\n
      \n
    • Nested list

      \n\n" + + "

      Nested header

    • \n
  • \n
\n", + } + + parameters := HTMLRendererParameters{ + HeadingIDPrefix: "PRE:", + HeadingIDSuffix: ":POST", + } + + doTestsParam(t, tests, TestParams{ + extensions: HeadingIDs, + HTMLFlags: UseXHTML, + HTMLRendererParameters: parameters, + }) +} + +func TestPrefixAutoHeaderIdExtension(t *testing.T) { + var tests = []string{ + "# Header 1\n", + "

Header 1

\n", + + "# Header 1 \n", + "

Header 1

\n", + + "## Header 2\n", + "

Header 2

\n", + + "### Header 3\n", + "

Header 3

\n", + + "#### Header 4\n", + "

Header 4

\n", + + "##### Header 5\n", + "
Header 5
\n", + + "###### Header 6\n", + "
Header 6
\n", + + "####### Header 7\n", + "
# Header 7
\n", + + "Hello\n# Header 1\nGoodbye\n", + "

Hello

\n\n

Header 1

\n\n

Goodbye

\n", + + "* List\n# Header\n* List\n", + "
    \n
  • List

    \n\n

    Header

  • \n\n
  • List

  • \n
\n", + + "* List\n#Header\n* List\n", + "
    \n
  • List

    \n\n

    Header

  • \n\n
  • List

  • \n
\n", + + "* List\n * Nested list\n # Nested header\n", + "
    \n
  • List

    \n\n
      \n
    • Nested list

      \n\n" + + "

      Nested header

    • \n
  • \n
\n", + + "# Header\n\n# Header\n", + "

Header

\n\n

Header

\n", + + "# Header 1\n\n# Header 1", + "

Header 1

\n\n

Header 1

\n", + + "# Header\n\n# Header 1\n\n# Header\n\n# Header", + "

Header

\n\n

Header 1

\n\n

Header

\n\n

Header

\n", + } + doTestsBlock(t, tests, AutoHeadingIDs) +} + +func TestPrefixAutoHeaderIdExtensionWithPrefixAndSuffix(t *testing.T) { + var tests = []string{ + "# Header 1\n", + "

Header 1

\n", + + "# Header 1 \n", + "

Header 1

\n", + + "## Header 2\n", + "

Header 2

\n", + + "### Header 3\n", + "

Header 3

\n", + + "#### Header 4\n", + "

Header 4

\n", + + "##### Header 5\n", + "
Header 5
\n", + + "###### Header 6\n", + "
Header 6
\n", + + "####### Header 7\n", + "
# Header 7
\n", + + "Hello\n# Header 1\nGoodbye\n", + "

Hello

\n\n

Header 1

\n\n

Goodbye

\n", + + "* List\n# Header\n* List\n", + "
    \n
  • List

    \n\n

    Header

  • \n\n
  • List

  • \n
\n", + + "* List\n#Header\n* List\n", + "
    \n
  • List

    \n\n

    Header

  • \n\n
  • List

  • \n
\n", + + "* List\n * Nested list\n # Nested header\n", + "
    \n
  • List

    \n\n
      \n
    • Nested list

      \n\n" + + "

      Nested header

    • \n
  • \n
\n", + + "# Header\n\n# Header\n", + "

Header

\n\n

Header

\n", + + "# Header 1\n\n# Header 1", + "

Header 1

\n\n

Header 1

\n", + + "# Header\n\n# Header 1\n\n# Header\n\n# Header", + "

Header

\n\n

Header 1

\n\n

Header

\n\n

Header

\n", + } + + parameters := HTMLRendererParameters{ + HeadingIDPrefix: "PRE:", + HeadingIDSuffix: ":POST", + } + + doTestsParam(t, tests, TestParams{ + extensions: AutoHeadingIDs, + HTMLFlags: UseXHTML, + HTMLRendererParameters: parameters, + }) +} + +func TestPrefixMultipleHeaderExtensions(t *testing.T) { + var tests = []string{ + "# Header\n\n# Header {#header}\n\n# Header 1", + "

Header

\n\n

Header

\n\n

Header 1

\n", + } + doTestsBlock(t, tests, AutoHeadingIDs|HeadingIDs) +} + +func TestUnderlineHeaders(t *testing.T) { + var tests = []string{ + "Header 1\n========\n", + "

Header 1

\n", + + "Header 2\n--------\n", + "

Header 2

\n", + + "A\n=\n", + "

A

\n", + + "B\n-\n", + "

B

\n", + + "Paragraph\nHeader\n=\n", + "

Paragraph

\n\n

Header

\n", + + "Header\n===\nParagraph\n", + "

Header

\n\n

Paragraph

\n", + + "Header\n===\nAnother header\n---\n", + "

Header

\n\n

Another header

\n", + + " Header\n======\n", + "

Header

\n", + + " Code\n========\n", + "
Code\n
\n\n

========

\n", + + "Header with *inline*\n=====\n", + "

Header with inline

\n", + + "* List\n * Sublist\n Not a header\n ------\n", + "
    \n
  • List\n\n
      \n
    • Sublist\nNot a header\n------
    • \n
  • \n
\n", + + "Paragraph\n\n\n\n\nHeader\n===\n", + "

Paragraph

\n\n

Header

\n", + + "Trailing space \n==== \n\n", + "

Trailing space

\n", + + "Trailing spaces\n==== \n\n", + "

Trailing spaces

\n", + + "Double underline\n=====\n=====\n", + "

Double underline

\n\n

=====

\n", + } + doTestsBlock(t, tests, 0) +} + +func TestUnderlineHeadersAutoIDs(t *testing.T) { + var tests = []string{ + "Header 1\n========\n", + "

Header 1

\n", + + "Header 2\n--------\n", + "

Header 2

\n", + + "A\n=\n", + "

A

\n", + + "B\n-\n", + "

B

\n", + + "Paragraph\nHeader\n=\n", + "

Paragraph

\n\n

Header

\n", + + "Header\n===\nParagraph\n", + "

Header

\n\n

Paragraph

\n", + + "Header\n===\nAnother header\n---\n", + "

Header

\n\n

Another header

\n", + + " Header\n======\n", + "

Header

\n", + + "Header with *inline*\n=====\n", + "

Header with inline

\n", + + "Paragraph\n\n\n\n\nHeader\n===\n", + "

Paragraph

\n\n

Header

\n", + + "Trailing space \n==== \n\n", + "

Trailing space

\n", + + "Trailing spaces\n==== \n\n", + "

Trailing spaces

\n", + + "Double underline\n=====\n=====\n", + "

Double underline

\n\n

=====

\n", + + "Header\n======\n\nHeader\n======\n", + "

Header

\n\n

Header

\n", + + "Header 1\n========\n\nHeader 1\n========\n", + "

Header 1

\n\n

Header 1

\n", + } + doTestsBlock(t, tests, AutoHeadingIDs) +} + +func TestHorizontalRule(t *testing.T) { + var tests = []string{ + "-\n", + "

-

\n", + + "--\n", + "

--

\n", + + "---\n", + "
\n", + + "----\n", + "
\n", + + "*\n", + "

*

\n", + + "**\n", + "

**

\n", + + "***\n", + "
\n", + + "****\n", + "
\n", + + "_\n", + "

_

\n", + + "__\n", + "

__

\n", + + "___\n", + "
\n", + + "____\n", + "
\n", + + "-*-\n", + "

-*-

\n", + + "- - -\n", + "
\n", + + "* * *\n", + "
\n", + + "_ _ _\n", + "
\n", + + "-----*\n", + "

-----*

\n", + + " ------ \n", + "
\n", + + "Hello\n***\n", + "

Hello

\n\n
\n", + + "---\n***\n___\n", + "
\n\n
\n\n
\n", + } + doTestsBlock(t, tests, 0) +} + +func TestUnorderedList(t *testing.T) { + var tests = []string{ + "* Hello\n", + "
    \n
  • Hello
  • \n
\n", + + "* Yin\n* Yang\n", + "
    \n
  • Yin
  • \n
  • Yang
  • \n
\n", + + "* Ting\n* Bong\n* Goo\n", + "
    \n
  • Ting
  • \n
  • Bong
  • \n
  • Goo
  • \n
\n", + + "* Yin\n\n* Yang\n", + "
    \n
  • Yin

  • \n\n
  • Yang

  • \n
\n", + + "* Ting\n\n* Bong\n* Goo\n", + "
    \n
  • Ting

  • \n\n
  • Bong

  • \n\n
  • Goo

  • \n
\n", + + "+ Hello\n", + "
    \n
  • Hello
  • \n
\n", + + "+ Yin\n+ Yang\n", + "
    \n
  • Yin
  • \n
  • Yang
  • \n
\n", + + "+ Ting\n+ Bong\n+ Goo\n", + "
    \n
  • Ting
  • \n
  • Bong
  • \n
  • Goo
  • \n
\n", + + "+ Yin\n\n+ Yang\n", + "
    \n
  • Yin

  • \n\n
  • Yang

  • \n
\n", + + "+ Ting\n\n+ Bong\n+ Goo\n", + "
    \n
  • Ting

  • \n\n
  • Bong

  • \n\n
  • Goo

  • \n
\n", + + "- Hello\n", + "
    \n
  • Hello
  • \n
\n", + + "- Yin\n- Yang\n", + "
    \n
  • Yin
  • \n
  • Yang
  • \n
\n", + + "- Ting\n- Bong\n- Goo\n", + "
    \n
  • Ting
  • \n
  • Bong
  • \n
  • Goo
  • \n
\n", + + "- Yin\n\n- Yang\n", + "
    \n
  • Yin

  • \n\n
  • Yang

  • \n
\n", + + "- Ting\n\n- Bong\n- Goo\n", + "
    \n
  • Ting

  • \n\n
  • Bong

  • \n\n
  • Goo

  • \n
\n", + + "*Hello\n", + "

*Hello

\n", + + "* Hello \n", + "
    \n
  • Hello
  • \n
\n", + + "* Hello \n Next line \n", + "
    \n
  • Hello\nNext line
  • \n
\n", + + "Paragraph\n* No linebreak\n", + "

Paragraph\n* No linebreak

\n", + + "Paragraph\n\n* Linebreak\n", + "

Paragraph

\n\n
    \n
  • Linebreak
  • \n
\n", + + "* List\n * Nested list\n", + "
    \n
  • List\n\n
      \n
    • Nested list
    • \n
  • \n
\n", + + "* List\n\n * Nested list\n", + "
    \n
  • List

    \n\n
      \n
    • Nested list
    • \n
  • \n
\n", + + "* List\n Second line\n\n + Nested\n", + "
    \n
  • List\nSecond line

    \n\n
      \n
    • Nested
    • \n
  • \n
\n", + + "* List\n + Nested\n\n Continued\n", + "
    \n
  • List

    \n\n
      \n
    • Nested
    • \n
    \n\n

    Continued

  • \n
\n", + + "* List\n * shallow indent\n", + "
    \n
  • List\n\n
      \n
    • shallow indent
    • \n
  • \n
\n", + + "* List\n" + + " * shallow indent\n" + + " * part of second list\n" + + " * still second\n" + + " * almost there\n" + + " * third level\n", + "
    \n" + + "
  • List\n\n" + + "
      \n" + + "
    • shallow indent
    • \n" + + "
    • part of second list
    • \n" + + "
    • still second
    • \n" + + "
    • almost there\n\n" + + "
        \n" + + "
      • third level
      • \n" + + "
    • \n" + + "
  • \n" + + "
\n", + + "* List\n extra indent, same paragraph\n", + "
    \n
  • List\n extra indent, same paragraph
  • \n
\n", + + "* List\n\n code block\n", + "
    \n
  • List

    \n\n
    code block\n
  • \n
\n", + + "* List\n\n code block with spaces\n", + "
    \n
  • List

    \n\n
      code block with spaces\n
  • \n
\n", + + "* List\n\n * sublist\n\n normal text\n\n * another sublist\n", + "
    \n
  • List

    \n\n
      \n
    • sublist
    • \n
    \n\n

    normal text

    \n\n
      \n
    • another sublist
    • \n
  • \n
\n", + } + doTestsBlock(t, tests, 0) +} + +func TestOrderedList(t *testing.T) { + var tests = []string{ + "1. Hello\n", + "
    \n
  1. Hello
  2. \n
\n", + + "1. Yin\n2. Yang\n", + "
    \n
  1. Yin
  2. \n
  3. Yang
  4. \n
\n", + + "1. Ting\n2. Bong\n3. Goo\n", + "
    \n
  1. Ting
  2. \n
  3. Bong
  4. \n
  5. Goo
  6. \n
\n", + + "1. Yin\n\n2. Yang\n", + "
    \n
  1. Yin

  2. \n\n
  3. Yang

  4. \n
\n", + + "1. Ting\n\n2. Bong\n3. Goo\n", + "
    \n
  1. Ting

  2. \n\n
  3. Bong

  4. \n\n
  5. Goo

  6. \n
\n", + + "1 Hello\n", + "

1 Hello

\n", + + "1.Hello\n", + "

1.Hello

\n", + + "1. Hello \n", + "
    \n
  1. Hello
  2. \n
\n", + + "1. Hello \n Next line \n", + "
    \n
  1. Hello\nNext line
  2. \n
\n", + + "Paragraph\n1. No linebreak\n", + "

Paragraph\n1. No linebreak

\n", + + "Paragraph\n\n1. Linebreak\n", + "

Paragraph

\n\n
    \n
  1. Linebreak
  2. \n
\n", + + "1. List\n 1. Nested list\n", + "
    \n
  1. List\n\n
      \n
    1. Nested list
    2. \n
  2. \n
\n", + + "1. List\n\n 1. Nested list\n", + "
    \n
  1. List

    \n\n
      \n
    1. Nested list
    2. \n
  2. \n
\n", + + "1. List\n Second line\n\n 1. Nested\n", + "
    \n
  1. List\nSecond line

    \n\n
      \n
    1. Nested
    2. \n
  2. \n
\n", + + "1. List\n 1. Nested\n\n Continued\n", + "
    \n
  1. List

    \n\n
      \n
    1. Nested
    2. \n
    \n\n

    Continued

  2. \n
\n", + + "1. List\n 1. shallow indent\n", + "
    \n
  1. List\n\n
      \n
    1. shallow indent
    2. \n
  2. \n
\n", + + "1. List\n" + + " 1. shallow indent\n" + + " 2. part of second list\n" + + " 3. still second\n" + + " 4. almost there\n" + + " 1. third level\n", + "
    \n" + + "
  1. List\n\n" + + "
      \n" + + "
    1. shallow indent
    2. \n" + + "
    3. part of second list
    4. \n" + + "
    5. still second
    6. \n" + + "
    7. almost there\n\n" + + "
        \n" + + "
      1. third level
      2. \n" + + "
    8. \n" + + "
  2. \n" + + "
\n", + + "1. List\n extra indent, same paragraph\n", + "
    \n
  1. List\n extra indent, same paragraph
  2. \n
\n", + + "1. List\n\n code block\n", + "
    \n
  1. List

    \n\n
    code block\n
  2. \n
\n", + + "1. List\n\n code block with spaces\n", + "
    \n
  1. List

    \n\n
      code block with spaces\n
  2. \n
\n", + + "1. List\n * Mixted list\n", + "
    \n
  1. List\n\n
      \n
    • Mixted list
    • \n
  2. \n
\n", + + "1. List\n * Mixed list\n", + "
    \n
  1. List\n\n
      \n
    • Mixed list
    • \n
  2. \n
\n", + + "* Start with unordered\n 1. Ordered\n", + "
    \n
  • Start with unordered\n\n
      \n
    1. Ordered
    2. \n
  • \n
\n", + + "* Start with unordered\n 1. Ordered\n", + "
    \n
  • Start with unordered\n\n
      \n
    1. Ordered
    2. \n
  • \n
\n", + + "1. numbers\n1. are ignored\n", + "
    \n
  1. numbers
  2. \n
  3. are ignored
  4. \n
\n", + } + doTestsBlock(t, tests, 0) +} + +func TestDefinitionList(t *testing.T) { + var tests = []string{ + "Term 1\n: Definition a\n", + "
\n
Term 1
\n
Definition a
\n
\n", + + "Term 1\n: Definition a \n", + "
\n
Term 1
\n
Definition a
\n
\n", + + "Term 1\n: Definition a\n: Definition b\n", + "
\n
Term 1
\n
Definition a
\n
Definition b
\n
\n", + + "Term 1\n: Definition a\n\nTerm 2\n: Definition b\n", + "
\n" + + "
Term 1
\n" + + "
Definition a
\n" + + "
Term 2
\n" + + "
Definition b
\n" + + "
\n", + + "Term 1\n: Definition a\n\nTerm 2\n: Definition b\n\nTerm 3\n: Definition c\n", + "
\n" + + "
Term 1
\n" + + "
Definition a
\n" + + "
Term 2
\n" + + "
Definition b
\n" + + "
Term 3
\n" + + "
Definition c
\n" + + "
\n", + + "Term 1\n: Definition a\n: Definition b\n\nTerm 2\n: Definition c\n", + "
\n" + + "
Term 1
\n" + + "
Definition a
\n" + + "
Definition b
\n" + + "
Term 2
\n" + + "
Definition c
\n" + + "
\n", + + "Term 1\n\n: Definition a\n\nTerm 2\n\n: Definition b\n", + "
\n" + + "
Term 1
\n" + + "

Definition a

\n" + + "
Term 2
\n" + + "

Definition b

\n" + + "
\n", + + "Term 1\n\n: Definition a\n\n: Definition b\n\nTerm 2\n\n: Definition c\n", + "
\n" + + "
Term 1
\n" + + "

Definition a

\n" + + "

Definition b

\n" + + "
Term 2
\n" + + "

Definition c

\n" + + "
\n", + + "Term 1\n: Definition a\nNext line\n", + "
\n
Term 1
\n
Definition a\nNext line
\n
\n", + + "Term 1\n: Definition a\n Next line\n", + "
\n
Term 1
\n
Definition a\nNext line
\n
\n", + + "Term 1\n: Definition a \n Next line \n", + "
\n
Term 1
\n
Definition a\nNext line
\n
\n", + + "Term 1\n: Definition a\nNext line\n\nTerm 2\n: Definition b", + "
\n" + + "
Term 1
\n" + + "
Definition a\nNext line
\n" + + "
Term 2
\n" + + "
Definition b
\n" + + "
\n", + + "Term 1\n: Definition a\n", + "
\n
Term 1
\n
Definition a
\n
\n", + + "Term 1\n:Definition a\n", + "

Term 1\n:Definition a

\n", + + "Term 1\n\n: Definition a\n\nTerm 2\n\n: Definition b\n\nText 1", + "
\n" + + "
Term 1
\n" + + "

Definition a

\n" + + "
Term 2
\n" + + "

Definition b

\n" + + "
\n" + + "\n

Text 1

\n", + + "Term 1\n\n: Definition a\n\nText 1\n\nTerm 2\n\n: Definition b\n\nText 2", + "
\n" + + "
Term 1
\n" + + "

Definition a

\n" + + "
\n" + + "\n

Text 1

\n" + + "\n
\n" + + "
Term 2
\n" + + "

Definition b

\n" + + "
\n" + + "\n

Text 2

\n", + } + doTestsBlock(t, tests, DefinitionLists) +} + +func TestPreformattedHtml(t *testing.T) { + var tests = []string{ + "
\n", + "
\n", + + "
\n
\n", + "
\n
\n", + + "
\n
\nParagraph\n", + "

\n
\nParagraph

\n", + + "
\n
\n", + "
\n
\n", + + "
\nAnything here\n
\n", + "
\nAnything here\n
\n", + + "
\n Anything here\n
\n", + "
\n Anything here\n
\n", + + "
\nAnything here\n
\n", + "
\nAnything here\n
\n", + + "
\nThis is *not* &proceessed\n
\n", + "
\nThis is *not* &proceessed\n
\n", + + "\n Something\n\n", + "

\n Something\n

\n", + + "
\n Something here\n\n", + "

\n Something here\n

\n", + + "Paragraph\n
\nHere? >&<\n
\n", + "

Paragraph\n

\nHere? >&<\n

\n", + + "Paragraph\n\n
\nHow about here? >&<\n
\n", + "

Paragraph

\n\n
\nHow about here? >&<\n
\n", + + "Paragraph\n
\nHere? >&<\n
\nAnd here?\n", + "

Paragraph\n

\nHere? >&<\n
\nAnd here?

\n", + + "Paragraph\n\n
\nHow about here? >&<\n
\nAnd here?\n", + "

Paragraph

\n\n

\nHow about here? >&<\n
\nAnd here?

\n", + + "Paragraph\n
\nHere? >&<\n
\n\nAnd here?\n", + "

Paragraph\n

\nHere? >&<\n

\n\n

And here?

\n", + + "Paragraph\n\n
\nHow about here? >&<\n
\n\nAnd here?\n", + "

Paragraph

\n\n
\nHow about here? >&<\n
\n\n

And here?

\n", + } + doTestsBlock(t, tests, 0) +} + +func TestPreformattedHtmlLax(t *testing.T) { + var tests = []string{ + "Paragraph\n
\nHere? >&<\n
\n", + "

Paragraph

\n\n
\nHere? >&<\n
\n", + + "Paragraph\n\n
\nHow about here? >&<\n
\n", + "

Paragraph

\n\n
\nHow about here? >&<\n
\n", + + "Paragraph\n
\nHere? >&<\n
\nAnd here?\n", + "

Paragraph

\n\n
\nHere? >&<\n
\n\n

And here?

\n", + + "Paragraph\n\n
\nHow about here? >&<\n
\nAnd here?\n", + "

Paragraph

\n\n
\nHow about here? >&<\n
\n\n

And here?

\n", + + "Paragraph\n
\nHere? >&<\n
\n\nAnd here?\n", + "

Paragraph

\n\n
\nHere? >&<\n
\n\n

And here?

\n", + + "Paragraph\n\n
\nHow about here? >&<\n
\n\nAnd here?\n", + "

Paragraph

\n\n
\nHow about here? >&<\n
\n\n

And here?

\n", + } + doTestsBlock(t, tests, LaxHTMLBlocks) +} + +func TestFencedCodeBlock(t *testing.T) { + var tests = []string{ + "``` go\nfunc foo() bool {\n\treturn true;\n}\n```\n", + "
func foo() bool {\n\treturn true;\n}\n
\n", + + "``` c\n/* special & char < > \" escaping */\n```\n", + "
/* special & char < > " escaping */\n
\n", + + "``` c\nno *inline* processing ~~of text~~\n```\n", + "
no *inline* processing ~~of text~~\n
\n", + + "```\nNo language\n```\n", + "
No language\n
\n", + + "``` {ocaml}\nlanguage in braces\n```\n", + "
language in braces\n
\n", + + "``` {ocaml} \nwith extra whitespace\n```\n", + "
with extra whitespace\n
\n", + + "```{ ocaml }\nwith extra whitespace\n```\n", + "
with extra whitespace\n
\n", + + "~ ~~ java\nWith whitespace\n~~~\n", + "

~ ~~ java\nWith whitespace\n~~~

\n", + + "~~\nonly two\n~~\n", + "

~~\nonly two\n~~

\n", + + "```` python\nextra\n````\n", + "
extra\n
\n", + + "~~~ perl\nthree to start, four to end\n~~~~\n", + "

~~~ perl\nthree to start, four to end\n~~~~

\n", + + "~~~~ perl\nfour to start, three to end\n~~~\n", + "

~~~~ perl\nfour to start, three to end\n~~~

\n", + + "~~~ bash\ntildes\n~~~\n", + "
tildes\n
\n", + + "``` lisp\nno ending\n", + "

``` lisp\nno ending

\n", + + "~~~ lisp\nend with language\n~~~ lisp\n", + "

~~~ lisp\nend with language\n~~~ lisp

\n", + + "```\nmismatched begin and end\n~~~\n", + "

```\nmismatched begin and end\n~~~

\n", + + "~~~\nmismatched begin and end\n```\n", + "

~~~\nmismatched begin and end\n```

\n", + + " ``` oz\nleading spaces\n```\n", + "
leading spaces\n
\n", + + " ``` oz\nleading spaces\n ```\n", + "
leading spaces\n
\n", + + " ``` oz\nleading spaces\n ```\n", + "
leading spaces\n
\n", + + "``` oz\nleading spaces\n ```\n", + "
leading spaces\n
\n", + + " ``` oz\nleading spaces\n ```\n", + "
``` oz\n
\n\n

leading spaces\n ```

\n", + + "Bla bla\n\n``` oz\ncode blocks breakup paragraphs\n```\n\nBla Bla\n", + "

Bla bla

\n\n
code blocks breakup paragraphs\n
\n\n

Bla Bla

\n", + + "Some text before a fenced code block\n``` oz\ncode blocks breakup paragraphs\n```\nAnd some text after a fenced code block", + "

Some text before a fenced code block

\n\n
code blocks breakup paragraphs\n
\n\n

And some text after a fenced code block

\n", + + "`", + "

`

\n", + + "Bla bla\n\n``` oz\ncode blocks breakup paragraphs\n```\n\nBla Bla\n\n``` oz\nmultiple code blocks work okay\n```\n\nBla Bla\n", + "

Bla bla

\n\n
code blocks breakup paragraphs\n
\n\n

Bla Bla

\n\n
multiple code blocks work okay\n
\n\n

Bla Bla

\n", + + "Some text before a fenced code block\n``` oz\ncode blocks breakup paragraphs\n```\nSome text in between\n``` oz\nmultiple code blocks work okay\n```\nAnd some text after a fenced code block", + "

Some text before a fenced code block

\n\n
code blocks breakup paragraphs\n
\n\n

Some text in between

\n\n
multiple code blocks work okay\n
\n\n

And some text after a fenced code block

\n", + + "```\n[]:()\n```\n", + "
[]:()\n
\n", + + "```\n[]:()\n[]:)\n[]:(\n[]:x\n[]:testing\n[:testing\n\n[]:\nlinebreak\n[]()\n\n[]:\n[]()\n```", + "
[]:()\n[]:)\n[]:(\n[]:x\n[]:testing\n[:testing\n\n[]:\nlinebreak\n[]()\n\n[]:\n[]()\n
\n", + } + doTestsBlock(t, tests, FencedCode) +} + +func TestFencedCodeInsideBlockquotes(t *testing.T) { + cat := func(s ...string) string { return strings.Join(s, "\n") } + var tests = []string{ + cat("> ```go", + "package moo", + "", + "```", + ""), + `
+
package moo
+
+
+
+`, + // ------------------------------------------- + cat("> foo", + "> ", + "> ```go", + "package moo", + "```", + "> ", + "> goo.", + ""), + `
+

foo

+ +
package moo
+
+ +

goo.

+
+`, + // ------------------------------------------- + cat("> foo", + "> ", + "> quote", + "continues", + "```", + ""), + `
+

foo

+ +

quote +continues +` + "```" + `

+
+`, + // ------------------------------------------- + cat("> foo", + "> ", + "> ```go", + "package moo", + "```", + "> ", + "> goo.", + "> ", + "> ```go", + "package zoo", + "```", + "> ", + "> woo.", + ""), + `
+

foo

+ +
package moo
+
+ +

goo.

+ +
package zoo
+
+ +

woo.

+
+`, + } + + // These 2 alternative forms of blockquoted fenced code blocks should produce same output. + forms := [2]string{ + cat("> plain quoted text", + "> ```fenced", + "code", + " with leading single space correctly preserved", + "okay", + "```", + "> rest of quoted text"), + cat("> plain quoted text", + "> ```fenced", + "> code", + "> with leading single space correctly preserved", + "> okay", + "> ```", + "> rest of quoted text"), + } + want := `
+

plain quoted text

+ +
code
+ with leading single space correctly preserved
+okay
+
+ +

rest of quoted text

+
+` + tests = append(tests, forms[0], want) + tests = append(tests, forms[1], want) + + doTestsBlock(t, tests, FencedCode) +} + +func TestTable(t *testing.T) { + var tests = []string{ + "a | b\n---|---\nc | d\n", + "\n\n\n\n\n\n\n\n" + + "\n\n\n\n\n\n
ab
cd
\n", + + "a | b\n---|--\nc | d\n", + "

a | b\n---|--\nc | d

\n", + + "|a|b|c|d|\n|----|----|----|---|\n|e|f|g|h|\n", + "\n\n\n\n\n\n\n\n\n\n" + + "\n\n\n\n\n\n\n\n
abcd
efgh
\n", + + "*a*|__b__|[c](C)|d\n---|---|---|---\ne|f|g|h\n", + "\n\n\n\n\n\n\n\n\n\n" + + "\n\n\n\n\n\n\n\n
abcd
efgh
\n", + + "a|b|c\n---|---|---\nd|e|f\ng|h\ni|j|k|l|m\nn|o|p\n", + "\n\n\n\n\n\n\n\n\n" + + "\n\n\n\n\n\n\n" + + "\n\n\n\n\n\n" + + "\n\n\n\n\n\n" + + "\n\n\n\n\n\n
abc
def
gh
ijk
nop
\n", + + "a|b|c\n---|---|---\n*d*|__e__|f\n", + "\n\n\n\n\n\n\n\n\n" + + "\n\n\n\n\n\n\n
abc
def
\n", + + "a|b|c|d\n:--|--:|:-:|---\ne|f|g|h\n", + "\n\n\n\n\n" + + "\n\n\n\n\n" + + "\n\n\n\n" + + "\n\n\n\n
abcd
efgh
\n", + + "a|b|c\n---|---|---\n", + "\n\n\n\n\n\n\n\n\n\n\n
abc
\n", + + "a| b|c | d | e\n---|---|---|---|---\nf| g|h | i |j\n", + "\n\n\n\n\n\n\n\n\n\n\n" + + "\n\n\n\n\n\n\n\n\n
abcde
fghij
\n", + + "a|b\\|c|d\n---|---|---\nf|g\\|h|i\n", + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ab|cd
fg|hi
\n", + } + doTestsBlock(t, tests, Tables) +} + +func TestUnorderedListWith_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK(t *testing.T) { + var tests = []string{ + "* Hello\n", + "
    \n
  • Hello
  • \n
\n", + + "* Yin\n* Yang\n", + "
    \n
  • Yin
  • \n
  • Yang
  • \n
\n", + + "* Ting\n* Bong\n* Goo\n", + "
    \n
  • Ting
  • \n
  • Bong
  • \n
  • Goo
  • \n
\n", + + "* Yin\n\n* Yang\n", + "
    \n
  • Yin

  • \n\n
  • Yang

  • \n
\n", + + "* Ting\n\n* Bong\n* Goo\n", + "
    \n
  • Ting

  • \n\n
  • Bong

  • \n\n
  • Goo

  • \n
\n", + + "+ Hello\n", + "
    \n
  • Hello
  • \n
\n", + + "+ Yin\n+ Yang\n", + "
    \n
  • Yin
  • \n
  • Yang
  • \n
\n", + + "+ Ting\n+ Bong\n+ Goo\n", + "
    \n
  • Ting
  • \n
  • Bong
  • \n
  • Goo
  • \n
\n", + + "+ Yin\n\n+ Yang\n", + "
    \n
  • Yin

  • \n\n
  • Yang

  • \n
\n", + + "+ Ting\n\n+ Bong\n+ Goo\n", + "
    \n
  • Ting

  • \n\n
  • Bong

  • \n\n
  • Goo

  • \n
\n", + + "- Hello\n", + "
    \n
  • Hello
  • \n
\n", + + "- Yin\n- Yang\n", + "
    \n
  • Yin
  • \n
  • Yang
  • \n
\n", + + "- Ting\n- Bong\n- Goo\n", + "
    \n
  • Ting
  • \n
  • Bong
  • \n
  • Goo
  • \n
\n", + + "- Yin\n\n- Yang\n", + "
    \n
  • Yin

  • \n\n
  • Yang

  • \n
\n", + + "- Ting\n\n- Bong\n- Goo\n", + "
    \n
  • Ting

  • \n\n
  • Bong

  • \n\n
  • Goo

  • \n
\n", + + "*Hello\n", + "

*Hello

\n", + + "* Hello \n", + "
    \n
  • Hello
  • \n
\n", + + "* Hello \n Next line \n", + "
    \n
  • Hello\nNext line
  • \n
\n", + + "Paragraph\n* No linebreak\n", + "

Paragraph

\n\n
    \n
  • No linebreak
  • \n
\n", + + "Paragraph\n\n* Linebreak\n", + "

Paragraph

\n\n
    \n
  • Linebreak
  • \n
\n", + + "* List\n * Nested list\n", + "
    \n
  • List\n\n
      \n
    • Nested list
    • \n
  • \n
\n", + + "* List\n\n * Nested list\n", + "
    \n
  • List

    \n\n
      \n
    • Nested list
    • \n
  • \n
\n", + + "* List\n Second line\n\n + Nested\n", + "
    \n
  • List\nSecond line

    \n\n
      \n
    • Nested
    • \n
  • \n
\n", + + "* List\n + Nested\n\n Continued\n", + "
    \n
  • List

    \n\n
      \n
    • Nested
    • \n
    \n\n

    Continued

  • \n
\n", + + "* List\n * shallow indent\n", + "
    \n
  • List\n\n
      \n
    • shallow indent
    • \n
  • \n
\n", + + "* List\n" + + " * shallow indent\n" + + " * part of second list\n" + + " * still second\n" + + " * almost there\n" + + " * third level\n", + "
    \n" + + "
  • List\n\n" + + "
      \n" + + "
    • shallow indent
    • \n" + + "
    • part of second list
    • \n" + + "
    • still second
    • \n" + + "
    • almost there\n\n" + + "
        \n" + + "
      • third level
      • \n" + + "
    • \n" + + "
  • \n" + + "
\n", + + "* List\n extra indent, same paragraph\n", + "
    \n
  • List\n extra indent, same paragraph
  • \n
\n", + + "* List\n\n code block\n", + "
    \n
  • List

    \n\n
    code block\n
  • \n
\n", + + "* List\n\n code block with spaces\n", + "
    \n
  • List

    \n\n
      code block with spaces\n
  • \n
\n", + + "* List\n\n * sublist\n\n normal text\n\n * another sublist\n", + "
    \n
  • List

    \n\n
      \n
    • sublist
    • \n
    \n\n

    normal text

    \n\n
      \n
    • another sublist
    • \n
  • \n
\n", + } + doTestsBlock(t, tests, NoEmptyLineBeforeBlock) +} + +func TestOrderedList_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK(t *testing.T) { + var tests = []string{ + "1. Hello\n", + "
    \n
  1. Hello
  2. \n
\n", + + "1. Yin\n2. Yang\n", + "
    \n
  1. Yin
  2. \n
  3. Yang
  4. \n
\n", + + "1. Ting\n2. Bong\n3. Goo\n", + "
    \n
  1. Ting
  2. \n
  3. Bong
  4. \n
  5. Goo
  6. \n
\n", + + "1. Yin\n\n2. Yang\n", + "
    \n
  1. Yin

  2. \n\n
  3. Yang

  4. \n
\n", + + "1. Ting\n\n2. Bong\n3. Goo\n", + "
    \n
  1. Ting

  2. \n\n
  3. Bong

  4. \n\n
  5. Goo

  6. \n
\n", + + "1 Hello\n", + "

1 Hello

\n", + + "1.Hello\n", + "

1.Hello

\n", + + "1. Hello \n", + "
    \n
  1. Hello
  2. \n
\n", + + "1. Hello \n Next line \n", + "
    \n
  1. Hello\nNext line
  2. \n
\n", + + "Paragraph\n1. No linebreak\n", + "

Paragraph

\n\n
    \n
  1. No linebreak
  2. \n
\n", + + "Paragraph\n\n1. Linebreak\n", + "

Paragraph

\n\n
    \n
  1. Linebreak
  2. \n
\n", + + "1. List\n 1. Nested list\n", + "
    \n
  1. List\n\n
      \n
    1. Nested list
    2. \n
  2. \n
\n", + + "1. List\n\n 1. Nested list\n", + "
    \n
  1. List

    \n\n
      \n
    1. Nested list
    2. \n
  2. \n
\n", + + "1. List\n Second line\n\n 1. Nested\n", + "
    \n
  1. List\nSecond line

    \n\n
      \n
    1. Nested
    2. \n
  2. \n
\n", + + "1. List\n 1. Nested\n\n Continued\n", + "
    \n
  1. List

    \n\n
      \n
    1. Nested
    2. \n
    \n\n

    Continued

  2. \n
\n", + + "1. List\n 1. shallow indent\n", + "
    \n
  1. List\n\n
      \n
    1. shallow indent
    2. \n
  2. \n
\n", + + "1. List\n" + + " 1. shallow indent\n" + + " 2. part of second list\n" + + " 3. still second\n" + + " 4. almost there\n" + + " 1. third level\n", + "
    \n" + + "
  1. List\n\n" + + "
      \n" + + "
    1. shallow indent
    2. \n" + + "
    3. part of second list
    4. \n" + + "
    5. still second
    6. \n" + + "
    7. almost there\n\n" + + "
        \n" + + "
      1. third level
      2. \n" + + "
    8. \n" + + "
  2. \n" + + "
\n", + + "1. List\n extra indent, same paragraph\n", + "
    \n
  1. List\n extra indent, same paragraph
  2. \n
\n", + + "1. List\n\n code block\n", + "
    \n
  1. List

    \n\n
    code block\n
  2. \n
\n", + + "1. List\n\n code block with spaces\n", + "
    \n
  1. List

    \n\n
      code block with spaces\n
  2. \n
\n", + + "1. List\n * Mixted list\n", + "
    \n
  1. List\n\n
      \n
    • Mixted list
    • \n
  2. \n
\n", + + "1. List\n * Mixed list\n", + "
    \n
  1. List\n\n
      \n
    • Mixed list
    • \n
  2. \n
\n", + + "* Start with unordered\n 1. Ordered\n", + "
    \n
  • Start with unordered\n\n
      \n
    1. Ordered
    2. \n
  • \n
\n", + + "* Start with unordered\n 1. Ordered\n", + "
    \n
  • Start with unordered\n\n
      \n
    1. Ordered
    2. \n
  • \n
\n", + + "1. numbers\n1. are ignored\n", + "
    \n
  1. numbers
  2. \n
  3. are ignored
  4. \n
\n", + } + doTestsBlock(t, tests, NoEmptyLineBeforeBlock) +} + +func TestFencedCodeBlock_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK(t *testing.T) { + var tests = []string{ + "``` go\nfunc foo() bool {\n\treturn true;\n}\n```\n", + "
func foo() bool {\n\treturn true;\n}\n
\n", + + "``` c\n/* special & char < > \" escaping */\n```\n", + "
/* special & char < > " escaping */\n
\n", + + "``` c\nno *inline* processing ~~of text~~\n```\n", + "
no *inline* processing ~~of text~~\n
\n", + + "```\nNo language\n```\n", + "
No language\n
\n", + + "``` {ocaml}\nlanguage in braces\n```\n", + "
language in braces\n
\n", + + "``` {ocaml} \nwith extra whitespace\n```\n", + "
with extra whitespace\n
\n", + + "```{ ocaml }\nwith extra whitespace\n```\n", + "
with extra whitespace\n
\n", + + "~ ~~ java\nWith whitespace\n~~~\n", + "

~ ~~ java\nWith whitespace\n~~~

\n", + + "~~\nonly two\n~~\n", + "

~~\nonly two\n~~

\n", + + "```` python\nextra\n````\n", + "
extra\n
\n", + + "~~~ perl\nthree to start, four to end\n~~~~\n", + "

~~~ perl\nthree to start, four to end\n~~~~

\n", + + "~~~~ perl\nfour to start, three to end\n~~~\n", + "

~~~~ perl\nfour to start, three to end\n~~~

\n", + + "~~~ bash\ntildes\n~~~\n", + "
tildes\n
\n", + + "``` lisp\nno ending\n", + "

``` lisp\nno ending

\n", + + "~~~ lisp\nend with language\n~~~ lisp\n", + "

~~~ lisp\nend with language\n~~~ lisp

\n", + + "```\nmismatched begin and end\n~~~\n", + "

```\nmismatched begin and end\n~~~

\n", + + "~~~\nmismatched begin and end\n```\n", + "

~~~\nmismatched begin and end\n```

\n", + + " ``` oz\nleading spaces\n```\n", + "
leading spaces\n
\n", + + " ``` oz\nleading spaces\n ```\n", + "
leading spaces\n
\n", + + " ``` oz\nleading spaces\n ```\n", + "
leading spaces\n
\n", + + "``` oz\nleading spaces\n ```\n", + "
leading spaces\n
\n", + + " ``` oz\nleading spaces\n ```\n", + "
``` oz\n
\n\n

leading spaces

\n\n
```\n
\n", + } + doTestsBlock(t, tests, FencedCode|NoEmptyLineBeforeBlock) +} + +func TestTitleBlock_EXTENSION_TITLEBLOCK(t *testing.T) { + var tests = []string{ + "% Some title\n" + + "% Another title line\n" + + "% Yep, more here too\n", + "

" + + "Some title\n" + + "Another title line\n" + + "Yep, more here too" + + "

\n", + } + doTestsBlock(t, tests, Titleblock) +} + +func TestBlockComments(t *testing.T) { + var tests = []string{ + "Some text\n\n\n", + "

Some text

\n\n\n", + + "Some text\n\n\n", + "

Some text

\n\n\n", + + "Some text\n\n\n", + "

Some text

\n\n\n", + } + doTestsBlock(t, tests, 0) +} + +func TestTOC(t *testing.T) { + var tests = []string{ + "# Title\n\n##Subtitle1\n\n##Subtitle2", + //"\n\n

Title

\n\n

Subtitle1

\n\n

Subtitle2

\n", + ` + +

Title

+ +

Subtitle1

+ +

Subtitle2

+`, + + "# Title\n\n##Subtitle\n\n#Title2", + //"\n\n

Title

\n\n

Subtitle

\n\n

Title2

\n", + ` + +

Title

+ +

Subtitle

+ +

Title2

+`, + + "## Subtitle\n\n# Title", + ` + +

Subtitle

+ +

Title

+`, + + "# Title 1\n\n## Subtitle 1\n\n### Subsubtitle 1\n\n# Title 2\n\n### Subsubtitle 2", + ` + +

Title 1

+ +

Subtitle 1

+ +

Subsubtitle 1

+ +

Title 2

+ +

Subsubtitle 2

+`, + + "# Title with `code`", + ` + +

Title with code

+`, + + // Trigger empty TOC + "#", + "", + } + doTestsParam(t, tests, TestParams{ + HTMLFlags: UseXHTML | TOC, + }) +} + +func TestCompletePage(t *testing.T) { + var tests = []string{ + "*foo*", + ` + + + + + + + + +

foo

+ + + +`, + } + doTestsParam(t, tests, TestParams{HTMLFlags: UseXHTML | CompletePage}) +} + +func TestIsFenceLine(t *testing.T) { + tests := []struct { + data []byte + syntaxRequested bool + wantEnd int + wantMarker string + wantSyntax string + }{ + { + data: []byte("```"), + wantEnd: 3, + wantMarker: "```", + }, + { + data: []byte("```\nstuff here\n"), + wantEnd: 4, + wantMarker: "```", + }, + { + data: []byte("```\nstuff here\n"), + syntaxRequested: true, + wantEnd: 4, + wantMarker: "```", + }, + { + data: []byte("stuff here\n```\n"), + wantEnd: 0, + }, + { + data: []byte("```"), + syntaxRequested: true, + wantEnd: 3, + wantMarker: "```", + }, + { + data: []byte("``` go"), + syntaxRequested: true, + wantEnd: 6, + wantMarker: "```", + wantSyntax: "go", + }, + } + + for _, test := range tests { + var syntax *string + if test.syntaxRequested { + syntax = new(string) + } + end, marker := isFenceLine(test.data, syntax, "```") + if got, want := end, test.wantEnd; got != want { + t.Errorf("got end %v, want %v", got, want) + } + if got, want := marker, test.wantMarker; got != want { + t.Errorf("got marker %q, want %q", got, want) + } + if test.syntaxRequested { + if got, want := *syntax, test.wantSyntax; got != want { + t.Errorf("got syntax %q, want %q", got, want) + } + } + } +} diff --git a/pkg/blackfriday/doc.go b/pkg/blackfriday/doc.go new file mode 100644 index 00000000..5b3fa987 --- /dev/null +++ b/pkg/blackfriday/doc.go @@ -0,0 +1,18 @@ +// Package blackfriday is a markdown processor. +// +// It translates plain text with simple formatting rules into an AST, which can +// then be further processed to HTML (provided by Blackfriday itself) or other +// formats (provided by the community). +// +// The simplest way to invoke Blackfriday is to call the Run function. It will +// take a text input and produce a text output in HTML (or other format). +// +// A slightly more sophisticated way to use Blackfriday is to create a Markdown +// processor and to call Parse, which returns a syntax tree for the input +// document. You can leverage Blackfriday's parsing for content extraction from +// markdown documents. You can assign a custom renderer and set various options +// to the Markdown processor. +// +// If you're interested in calling Blackfriday from command line, see +// https://github.com/russross/blackfriday-tool. +package blackfriday diff --git a/pkg/blackfriday/esc.go b/pkg/blackfriday/esc.go new file mode 100644 index 00000000..6385f27c --- /dev/null +++ b/pkg/blackfriday/esc.go @@ -0,0 +1,34 @@ +package blackfriday + +import ( + "html" + "io" +) + +var htmlEscaper = [256][]byte{ + '&': []byte("&"), + '<': []byte("<"), + '>': []byte(">"), + '"': []byte("""), +} + +func escapeHTML(w io.Writer, s []byte) { + var start, end int + for end < len(s) { + escSeq := htmlEscaper[s[end]] + if escSeq != nil { + w.Write(s[start:end]) + w.Write(escSeq) + start = end + 1 + } + end++ + } + if start < len(s) && end <= len(s) { + w.Write(s[start:end]) + } +} + +func escLink(w io.Writer, text []byte) { + unesc := html.UnescapeString(string(text)) + escapeHTML(w, []byte(unesc)) +} diff --git a/pkg/blackfriday/esc_test.go b/pkg/blackfriday/esc_test.go new file mode 100644 index 00000000..ff67d546 --- /dev/null +++ b/pkg/blackfriday/esc_test.go @@ -0,0 +1,48 @@ +package blackfriday + +import ( + "bytes" + "testing" +) + +func TestEsc(t *testing.T) { + tests := []string{ + "abc", "abc", + "a&c", "a&c", + "<", "<", + "[]:<", "[]:<", + "Hello |" + processingInstruction = "[<][?].*?[?][>]" + singleQuotedValue = "'[^']*'" + tagName = "[A-Za-z][A-Za-z0-9-]*" + unquotedValue = "[^\"'=<>`\\x00-\\x20]+" +) + +// HTMLRendererParameters is a collection of supplementary parameters tweaking +// the behavior of various parts of HTML renderer. +type HTMLRendererParameters struct { + // Prepend this text to each relative URL. + AbsolutePrefix string + // Add this text to each footnote anchor, to ensure uniqueness. + FootnoteAnchorPrefix string + // Show this text inside the tag for a footnote return link, if the + // HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string + // [return] is used. + FootnoteReturnLinkContents string + // If set, add this text to the front of each Heading ID, to ensure + // uniqueness. + HeadingIDPrefix string + // If set, add this text to the back of each Heading ID, to ensure uniqueness. + HeadingIDSuffix string + + Title string // Document title (used if CompletePage is set) + CSS string // Optional CSS file URL (used if CompletePage is set) + Icon string // Optional icon file URL (used if CompletePage is set) + + Flags HTMLFlags // Flags allow customizing this renderer's behavior +} + +// HTMLRenderer is a type that implements the Renderer interface for HTML output. +// +// Do not create this directly, instead use the NewHTMLRenderer function. +type HTMLRenderer struct { + HTMLRendererParameters + + closeTag string // how to end singleton tags: either " />" or ">" + + // Track heading IDs to prevent ID collision in a single generation. + headingIDs map[string]int + + lastOutputLen int + disableTags int + + sr *SPRenderer +} + +const ( + xhtmlClose = " />" + htmlClose = ">" +) + +// NewHTMLRenderer creates and configures an HTMLRenderer object, which +// satisfies the Renderer interface. +func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer { + // configure the rendering engine + closeTag := htmlClose + if params.Flags&UseXHTML != 0 { + closeTag = xhtmlClose + } + + if params.FootnoteReturnLinkContents == "" { + params.FootnoteReturnLinkContents = `[return]` + } + + return &HTMLRenderer{ + HTMLRendererParameters: params, + + closeTag: closeTag, + headingIDs: make(map[string]int), + + sr: NewSmartypantsRenderer(params.Flags), + } +} + +func isHTMLTag(tag []byte, tagname string) bool { + found, _ := findHTMLTagPos(tag, tagname) + return found +} + +// Look for a character, but ignore it when it's in any kind of quotes, it +// might be JavaScript +func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int { + inSingleQuote := false + inDoubleQuote := false + inGraveQuote := false + i := start + for i < len(html) { + switch { + case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote: + return i + case html[i] == '\'': + inSingleQuote = !inSingleQuote + case html[i] == '"': + inDoubleQuote = !inDoubleQuote + case html[i] == '`': + inGraveQuote = !inGraveQuote + } + i++ + } + return start +} + +func findHTMLTagPos(tag []byte, tagname string) (bool, int) { + i := 0 + if i < len(tag) && tag[0] != '<' { + return false, -1 + } + i++ + i = skipSpace(tag, i) + + if i < len(tag) && tag[i] == '/' { + i++ + } + + i = skipSpace(tag, i) + j := 0 + for ; i < len(tag); i, j = i+1, j+1 { + if j >= len(tagname) { + break + } + + if strings.ToLower(string(tag[i]))[0] != tagname[j] { + return false, -1 + } + } + + if i == len(tag) { + return false, -1 + } + + rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>') + if rightAngle >= i { + return true, rightAngle + } + + return false, -1 +} + +func skipSpace(tag []byte, i int) int { + for i < len(tag) && isspace(tag[i]) { + i++ + } + return i +} + +func isRelativeLink(link []byte) (yes bool) { + // a tag begin with '#' + if link[0] == '#' { + return true + } + + // link begin with '/' but not '//', the second maybe a protocol relative link + if len(link) >= 2 && link[0] == '/' && link[1] != '/' { + return true + } + + // only the root '/' + if len(link) == 1 && link[0] == '/' { + return true + } + + // current directory : begin with "./" + if bytes.HasPrefix(link, []byte("./")) { + return true + } + + // parent directory : begin with "../" + if bytes.HasPrefix(link, []byte("../")) { + return true + } + + return false +} + +func (r *HTMLRenderer) ensureUniqueHeadingID(id string) string { + for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] { + tmp := fmt.Sprintf("%s-%d", id, count+1) + + if _, tmpFound := r.headingIDs[tmp]; !tmpFound { + r.headingIDs[id] = count + 1 + id = tmp + } else { + id = id + "-1" + } + } + + if _, found := r.headingIDs[id]; !found { + r.headingIDs[id] = 0 + } + + return id +} + +func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte { + if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' { + newDest := r.AbsolutePrefix + if link[0] != '/' { + newDest += "/" + } + newDest += string(link) + return []byte(newDest) + } + return link +} + +func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string { + if isRelativeLink(link) { + return attrs + } + val := []string{} + if flags&NofollowLinks != 0 { + val = append(val, "nofollow") + } + if flags&NoreferrerLinks != 0 { + val = append(val, "noreferrer") + } + if flags&HrefTargetBlank != 0 { + attrs = append(attrs, "target=\"_blank\"") + } + if len(val) == 0 { + return attrs + } + attr := fmt.Sprintf("rel=%q", strings.Join(val, " ")) + return append(attrs, attr) +} + +func isMailto(link []byte) bool { + return bytes.HasPrefix(link, []byte("mailto:")) +} + +func needSkipLink(flags HTMLFlags, dest []byte) bool { + if flags&SkipLinks != 0 { + return true + } + return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest) +} + +func isSmartypantable(node *Node) bool { + pt := node.Parent.Type + return pt != Link && pt != CodeBlock && pt != Code +} + +func appendLanguageAttr(attrs []string, info []byte) []string { + if len(info) == 0 { + return attrs + } + endOfLang := bytes.IndexAny(info, "\t ") + if endOfLang < 0 { + endOfLang = len(info) + } + return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang])) +} + +func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) { + w.Write(name) + if len(attrs) > 0 { + w.Write(spaceBytes) + w.Write([]byte(strings.Join(attrs, " "))) + } + w.Write(gtBytes) + r.lastOutputLen = 1 +} + +func footnoteRef(prefix string, node *Node) []byte { + urlFrag := prefix + string(slugify(node.Destination)) + anchor := fmt.Sprintf(`%d`, urlFrag, node.NoteID) + return []byte(fmt.Sprintf(`%s`, urlFrag, anchor)) +} + +func footnoteItem(prefix string, slug []byte) []byte { + return []byte(fmt.Sprintf(`
  • `, prefix, slug)) +} + +func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte { + const format = ` %s` + return []byte(fmt.Sprintf(format, prefix, slug, returnLink)) +} + +func itemOpenCR(node *Node) bool { + if node.Prev == nil { + return false + } + ld := node.Parent.ListData + return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0 +} + +func skipParagraphTags(node *Node) bool { + grandparent := node.Parent.Parent + if grandparent == nil || grandparent.Type != List { + return false + } + tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0 + return grandparent.Type == List && tightOrTerm +} + +func cellAlignment(align CellAlignFlags) string { + switch align { + case TableAlignmentLeft: + return "left" + case TableAlignmentRight: + return "right" + case TableAlignmentCenter: + return "center" + default: + return "" + } +} + +func (r *HTMLRenderer) out(w io.Writer, text []byte) { + if r.disableTags > 0 { + w.Write(htmlTagRe.ReplaceAll(text, []byte{})) + } else { + w.Write(text) + } + r.lastOutputLen = len(text) +} + +func (r *HTMLRenderer) cr(w io.Writer) { + if r.lastOutputLen > 0 { + r.out(w, nlBytes) + } +} + +var ( + nlBytes = []byte{'\n'} + gtBytes = []byte{'>'} + spaceBytes = []byte{' '} +) + +var ( + brTag = []byte("
    ") + brXHTMLTag = []byte("
    ") + emTag = []byte("") + emCloseTag = []byte("") + strongTag = []byte("") + strongCloseTag = []byte("") + delTag = []byte("") + delCloseTag = []byte("") + ttTag = []byte("") + ttCloseTag = []byte("") + aTag = []byte("") + preTag = []byte("
    ")
    +	preCloseTag        = []byte("
    ") + codeTag = []byte("") + codeCloseTag = []byte("") + pTag = []byte("

    ") + pCloseTag = []byte("

    ") + blockquoteTag = []byte("
    ") + blockquoteCloseTag = []byte("
    ") + hrTag = []byte("
    ") + hrXHTMLTag = []byte("
    ") + ulTag = []byte("
      ") + ulCloseTag = []byte("
    ") + olTag = []byte("
      ") + olCloseTag = []byte("
    ") + dlTag = []byte("
    ") + dlCloseTag = []byte("
    ") + liTag = []byte("
  • ") + liCloseTag = []byte("
  • ") + ddTag = []byte("
    ") + ddCloseTag = []byte("
    ") + dtTag = []byte("
    ") + dtCloseTag = []byte("
    ") + tableTag = []byte("") + tableCloseTag = []byte("
    ") + tdTag = []byte("") + thTag = []byte("") + theadTag = []byte("") + theadCloseTag = []byte("") + tbodyTag = []byte("") + tbodyCloseTag = []byte("") + trTag = []byte("") + trCloseTag = []byte("") + h1Tag = []byte("") + h2Tag = []byte("") + h3Tag = []byte("") + h4Tag = []byte("") + h5Tag = []byte("") + h6Tag = []byte("") + + footnotesDivBytes = []byte("\n
    \n\n") + footnotesCloseDivBytes = []byte("\n
    \n") +) + +func headingTagsFromLevel(level int) ([]byte, []byte) { + switch level { + case 1: + return h1Tag, h1CloseTag + case 2: + return h2Tag, h2CloseTag + case 3: + return h3Tag, h3CloseTag + case 4: + return h4Tag, h4CloseTag + case 5: + return h5Tag, h5CloseTag + default: + return h6Tag, h6CloseTag + } +} + +func (r *HTMLRenderer) outHRTag(w io.Writer) { + if r.Flags&UseXHTML == 0 { + r.out(w, hrTag) + } else { + r.out(w, hrXHTMLTag) + } +} + +// RenderNode is a default renderer of a single node of a syntax tree. For +// block nodes it will be called twice: first time with entering=true, second +// time with entering=false, so that it could know when it's working on an open +// tag and when on close. It writes the result to w. +// +// The return value is a way to tell the calling walker to adjust its walk +// pattern: e.g. it can terminate the traversal by returning Terminate. Or it +// can ask the walker to skip a subtree of this node by returning SkipChildren. +// The typical behavior is to return GoToNext, which asks for the usual +// traversal to the next node. +func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus { + attrs := []string{} + switch node.Type { + case Text: + if r.Flags&Smartypants != 0 { + var tmp bytes.Buffer + escapeHTML(&tmp, node.Literal) + r.sr.Process(w, tmp.Bytes()) + } else { + if node.Parent.Type == Link { + escLink(w, node.Literal) + } else { + escapeHTML(w, node.Literal) + } + } + case Softbreak: + r.cr(w) + // TODO: make it configurable via out(renderer.softbreak) + case Hardbreak: + if r.Flags&UseXHTML == 0 { + r.out(w, brTag) + } else { + r.out(w, brXHTMLTag) + } + r.cr(w) + case Emph: + if entering { + r.out(w, emTag) + } else { + r.out(w, emCloseTag) + } + case Strong: + if entering { + r.out(w, strongTag) + } else { + r.out(w, strongCloseTag) + } + case Del: + if entering { + r.out(w, delTag) + } else { + r.out(w, delCloseTag) + } + case HTMLSpan: + if r.Flags&SkipHTML != 0 { + break + } + r.out(w, node.Literal) + case Link: + // mark it but don't link it if it is not a safe link: no smartypants + dest := node.LinkData.Destination + if needSkipLink(r.Flags, dest) { + if entering { + r.out(w, ttTag) + } else { + r.out(w, ttCloseTag) + } + } else { + if entering { + dest = r.addAbsPrefix(dest) + var hrefBuf bytes.Buffer + hrefBuf.WriteString("href=\"") + escLink(&hrefBuf, dest) + hrefBuf.WriteByte('"') + attrs = append(attrs, hrefBuf.String()) + if node.NoteID != 0 { + r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node)) + break + } + attrs = appendLinkAttrs(attrs, r.Flags, dest) + if len(node.LinkData.Title) > 0 { + var titleBuff bytes.Buffer + titleBuff.WriteString("title=\"") + escapeHTML(&titleBuff, node.LinkData.Title) + titleBuff.WriteByte('"') + attrs = append(attrs, titleBuff.String()) + } + r.tag(w, aTag, attrs) + } else { + if node.NoteID != 0 { + break + } + r.out(w, aCloseTag) + } + } + case Image: + if r.Flags&SkipImages != 0 { + return SkipChildren + } + if entering { + dest := node.LinkData.Destination + dest = r.addAbsPrefix(dest) + if r.disableTags == 0 { + //if options.safe && potentiallyUnsafe(dest) { + //out(w, ``)
+				//} else {
+				r.out(w, []byte(`<img src=`)) + } + } + case Code: + r.out(w, codeTag) + escapeHTML(w, node.Literal) + r.out(w, codeCloseTag) + case Document: + break + case Paragraph: + if skipParagraphTags(node) { + break + } + if entering { + // TODO: untangle this clusterfuck about when the newlines need + // to be added and when not. + if node.Prev != nil { + switch node.Prev.Type { + case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule: + r.cr(w) + } + } + if node.Parent.Type == BlockQuote && node.Prev == nil { + r.cr(w) + } + r.out(w, pTag) + } else { + r.out(w, pCloseTag) + if !(node.Parent.Type == Item && node.Next == nil) { + r.cr(w) + } + } + case BlockQuote: + if entering { + r.cr(w) + r.out(w, blockquoteTag) + } else { + r.out(w, blockquoteCloseTag) + r.cr(w) + } + case HTMLBlock: + if r.Flags&SkipHTML != 0 { + break + } + r.cr(w) + r.out(w, node.Literal) + r.cr(w) + case Heading: + openTag, closeTag := headingTagsFromLevel(node.Level) + if entering { + if node.IsTitleblock { + attrs = append(attrs, `class="title"`) + } + if node.HeadingID != "" { + id := r.ensureUniqueHeadingID(node.HeadingID) + if r.HeadingIDPrefix != "" { + id = r.HeadingIDPrefix + id + } + if r.HeadingIDSuffix != "" { + id = id + r.HeadingIDSuffix + } + attrs = append(attrs, fmt.Sprintf(`id="%s"`, id)) + } + r.cr(w) + r.tag(w, openTag, attrs) + } else { + r.out(w, closeTag) + if !(node.Parent.Type == Item && node.Next == nil) { + r.cr(w) + } + } + case HorizontalRule: + r.cr(w) + r.outHRTag(w) + r.cr(w) + case List: + openTag := ulTag + closeTag := ulCloseTag + if node.ListFlags&ListTypeOrdered != 0 { + openTag = olTag + closeTag = olCloseTag + } + if node.ListFlags&ListTypeDefinition != 0 { + openTag = dlTag + closeTag = dlCloseTag + } + if entering { + if node.IsFootnotesList { + r.out(w, footnotesDivBytes) + r.outHRTag(w) + r.cr(w) + } + r.cr(w) + if node.Parent.Type == Item && node.Parent.Parent.Tight { + r.cr(w) + } + r.tag(w, openTag[:len(openTag)-1], attrs) + r.cr(w) + } else { + r.out(w, closeTag) + //cr(w) + //if node.parent.Type != Item { + // cr(w) + //} + if node.Parent.Type == Item && node.Next != nil { + r.cr(w) + } + if node.Parent.Type == Document || node.Parent.Type == BlockQuote { + r.cr(w) + } + if node.IsFootnotesList { + r.out(w, footnotesCloseDivBytes) + } + } + case Item: + openTag := liTag + closeTag := liCloseTag + if node.ListFlags&ListTypeDefinition != 0 { + openTag = ddTag + closeTag = ddCloseTag + } + if node.ListFlags&ListTypeTerm != 0 { + openTag = dtTag + closeTag = dtCloseTag + } + if entering { + if itemOpenCR(node) { + r.cr(w) + } + if node.ListData.RefLink != nil { + slug := slugify(node.ListData.RefLink) + r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug)) + break + } + r.out(w, openTag) + } else { + if node.ListData.RefLink != nil { + slug := slugify(node.ListData.RefLink) + if r.Flags&FootnoteReturnLinks != 0 { + r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug)) + } + } + r.out(w, closeTag) + r.cr(w) + } + case CodeBlock: + attrs = appendLanguageAttr(attrs, node.Info) + r.cr(w) + r.out(w, preTag) + r.tag(w, codeTag[:len(codeTag)-1], attrs) + escapeHTML(w, node.Literal) + r.out(w, codeCloseTag) + r.out(w, preCloseTag) + if node.Parent.Type != Item { + r.cr(w) + } + case Table: + if entering { + r.cr(w) + r.out(w, tableTag) + } else { + r.out(w, tableCloseTag) + r.cr(w) + } + case TableCell: + openTag := tdTag + closeTag := tdCloseTag + if node.IsHeader { + openTag = thTag + closeTag = thCloseTag + } + if entering { + align := cellAlignment(node.Align) + if align != "" { + attrs = append(attrs, fmt.Sprintf(`align="%s"`, align)) + } + if node.Prev == nil { + r.cr(w) + } + r.tag(w, openTag, attrs) + } else { + r.out(w, closeTag) + r.cr(w) + } + case TableHead: + if entering { + r.cr(w) + r.out(w, theadTag) + } else { + r.out(w, theadCloseTag) + r.cr(w) + } + case TableBody: + if entering { + r.cr(w) + r.out(w, tbodyTag) + // XXX: this is to adhere to a rather silly test. Should fix test. + if node.FirstChild == nil { + r.cr(w) + } + } else { + r.out(w, tbodyCloseTag) + r.cr(w) + } + case TableRow: + if entering { + r.cr(w) + r.out(w, trTag) + } else { + r.out(w, trCloseTag) + r.cr(w) + } + default: + panic("Unknown node type " + node.Type.String()) + } + return GoToNext +} + +// RenderHeader writes HTML document preamble and TOC if requested. +func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) { + r.writeDocumentHeader(w) + if r.Flags&TOC != 0 { + r.writeTOC(w, ast) + } +} + +// RenderFooter writes HTML document footer. +func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) { + if r.Flags&CompletePage == 0 { + return + } + io.WriteString(w, "\n\n\n") +} + +func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) { + if r.Flags&CompletePage == 0 { + return + } + ending := "" + if r.Flags&UseXHTML != 0 { + io.WriteString(w, "\n") + io.WriteString(w, "\n") + ending = " /" + } else { + io.WriteString(w, "\n") + io.WriteString(w, "\n") + } + io.WriteString(w, "\n") + io.WriteString(w, " ") + if r.Flags&Smartypants != 0 { + r.sr.Process(w, []byte(r.Title)) + } else { + escapeHTML(w, []byte(r.Title)) + } + io.WriteString(w, "\n") + io.WriteString(w, " \n") + io.WriteString(w, " \n") + if r.CSS != "" { + io.WriteString(w, " \n") + } + if r.Icon != "" { + io.WriteString(w, " \n") + } + io.WriteString(w, "\n") + io.WriteString(w, "\n\n") +} + +func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) { + buf := bytes.Buffer{} + + inHeading := false + tocLevel := 0 + headingCount := 0 + + ast.Walk(func(node *Node, entering bool) WalkStatus { + if node.Type == Heading && !node.HeadingData.IsTitleblock { + inHeading = entering + if entering { + node.HeadingID = fmt.Sprintf("toc_%d", headingCount) + if node.Level == tocLevel { + buf.WriteString("\n\n
  • ") + } else if node.Level < tocLevel { + for node.Level < tocLevel { + tocLevel-- + buf.WriteString("
  • \n") + } + buf.WriteString("\n\n
  • ") + } else { + for node.Level > tocLevel { + tocLevel++ + buf.WriteString("\n") + } + + if buf.Len() > 0 { + io.WriteString(w, "\n") + } + r.lastOutputLen = buf.Len() +} diff --git a/pkg/blackfriday/inline.go b/pkg/blackfriday/inline.go new file mode 100644 index 00000000..3d633106 --- /dev/null +++ b/pkg/blackfriday/inline.go @@ -0,0 +1,1214 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// Functions to parse inline elements. +// + +package blackfriday + +import ( + "bytes" + "regexp" + "strconv" +) + +var ( + urlRe = `((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+` + anchorRe = regexp.MustCompile(`^(]+")?\s?>` + urlRe + `<\/a>)`) + + // TODO: improve this regexp to catch all possible entities: + htmlEntityRe = regexp.MustCompile(`&[a-z]{2,5};`) +) + +// Functions to parse text within a block +// Each function returns the number of chars taken care of +// data is the complete block being rendered +// offset is the number of valid chars before the current cursor + +func (p *Markdown) inline(currBlock *Node, data []byte) { + // handlers might call us recursively: enforce a maximum depth + if p.nesting >= p.maxNesting || len(data) == 0 { + return + } + p.nesting++ + beg, end := 0, 0 + for end < len(data) { + handler := p.inlineCallback[data[end]] + if handler != nil { + if consumed, node := handler(p, data, end); consumed == 0 { + // No action from the callback. + end++ + } else { + // Copy inactive chars into the output. + currBlock.AppendChild(text(data[beg:end])) + if node != nil { + currBlock.AppendChild(node) + } + // Skip past whatever the callback used. + beg = end + consumed + end = beg + } + } else { + end++ + } + } + if beg < len(data) { + if data[end-1] == '\n' { + end-- + } + currBlock.AppendChild(text(data[beg:end])) + } + p.nesting-- +} + +// single and double emphasis parsing +func emphasis(p *Markdown, data []byte, offset int) (int, *Node) { + data = data[offset:] + c := data[0] + + if len(data) > 2 && data[1] != c { + // whitespace cannot follow an opening emphasis; + // strikethrough only takes two characters '~~' + if c == '~' || isspace(data[1]) { + return 0, nil + } + ret, node := helperEmphasis(p, data[1:], c) + if ret == 0 { + return 0, nil + } + + return ret + 1, node + } + + if len(data) > 3 && data[1] == c && data[2] != c { + if isspace(data[2]) { + return 0, nil + } + ret, node := helperDoubleEmphasis(p, data[2:], c) + if ret == 0 { + return 0, nil + } + + return ret + 2, node + } + + if len(data) > 4 && data[1] == c && data[2] == c && data[3] != c { + if c == '~' || isspace(data[3]) { + return 0, nil + } + ret, node := helperTripleEmphasis(p, data, 3, c) + if ret == 0 { + return 0, nil + } + + return ret + 3, node + } + + return 0, nil +} + +func codeSpan(p *Markdown, data []byte, offset int) (int, *Node) { + data = data[offset:] + + nb := 0 + + // count the number of backticks in the delimiter + for nb < len(data) && data[nb] == '`' { + nb++ + } + + // find the next delimiter + i, end := 0, 0 + for end = nb; end < len(data) && i < nb; end++ { + if data[end] == '`' { + i++ + } else { + i = 0 + } + } + + // no matching delimiter? + if i < nb && end >= len(data) { + return 0, nil + } + + // trim outside whitespace + fBegin := nb + for fBegin < end && data[fBegin] == ' ' { + fBegin++ + } + + fEnd := end - nb + for fEnd > fBegin && data[fEnd-1] == ' ' { + fEnd-- + } + + // render the code span + if fBegin != fEnd { + code := NewNode(Code) + code.Literal = data[fBegin:fEnd] + return end, code + } + + return end, nil +} + +// newline preceded by two spaces becomes
    +func maybeLineBreak(p *Markdown, data []byte, offset int) (int, *Node) { + origOffset := offset + for offset < len(data) && data[offset] == ' ' { + offset++ + } + + if offset < len(data) && data[offset] == '\n' { + if offset-origOffset >= 2 { + return offset - origOffset + 1, NewNode(Hardbreak) + } + return offset - origOffset, nil + } + return 0, nil +} + +// newline without two spaces works when HardLineBreak is enabled +func lineBreak(p *Markdown, data []byte, offset int) (int, *Node) { + if p.extensions&HardLineBreak != 0 { + return 1, NewNode(Hardbreak) + } + return 0, nil +} + +type linkType int + +const ( + linkNormal linkType = iota + linkImg + linkDeferredFootnote + linkInlineFootnote +) + +func isReferenceStyleLink(data []byte, pos int, t linkType) bool { + if t == linkDeferredFootnote { + return false + } + return pos < len(data)-1 && data[pos] == '[' && data[pos+1] != '^' +} + +func maybeImage(p *Markdown, data []byte, offset int) (int, *Node) { + if offset < len(data)-1 && data[offset+1] == '[' { + return link(p, data, offset) + } + return 0, nil +} + +func maybeInlineFootnote(p *Markdown, data []byte, offset int) (int, *Node) { + if offset < len(data)-1 && data[offset+1] == '[' { + return link(p, data, offset) + } + return 0, nil +} + +// '[': parse a link or an image or a footnote +func link(p *Markdown, data []byte, offset int) (int, *Node) { + // no links allowed inside regular links, footnote, and deferred footnotes + if p.insideLink && (offset > 0 && data[offset-1] == '[' || len(data)-1 > offset && data[offset+1] == '^') { + return 0, nil + } + + var t linkType + switch { + // special case: ![^text] == deferred footnote (that follows something with + // an exclamation point) + case p.extensions&Footnotes != 0 && len(data)-1 > offset && data[offset+1] == '^': + t = linkDeferredFootnote + // ![alt] == image + case offset >= 0 && data[offset] == '!': + t = linkImg + offset++ + // ^[text] == inline footnote + // [^refId] == deferred footnote + case p.extensions&Footnotes != 0: + if offset >= 0 && data[offset] == '^' { + t = linkInlineFootnote + offset++ + } else if len(data)-1 > offset && data[offset+1] == '^' { + t = linkDeferredFootnote + } + // [text] == regular link + default: + t = linkNormal + } + + data = data[offset:] + + var ( + i = 1 + noteID int + title, link, altContent []byte + textHasNl = false + ) + + if t == linkDeferredFootnote { + i++ + } + + // look for the matching closing bracket + for level := 1; level > 0 && i < len(data); i++ { + switch { + case data[i] == '\n': + textHasNl = true + + case data[i-1] == '\\': + continue + + case data[i] == '[': + level++ + + case data[i] == ']': + level-- + if level <= 0 { + i-- // compensate for extra i++ in for loop + } + } + } + + if i >= len(data) { + return 0, nil + } + + txtE := i + i++ + var footnoteNode *Node + + // skip any amount of whitespace or newline + // (this is much more lax than original markdown syntax) + for i < len(data) && isspace(data[i]) { + i++ + } + + // inline style link + switch { + case i < len(data) && data[i] == '(': + // skip initial whitespace + i++ + + for i < len(data) && isspace(data[i]) { + i++ + } + + linkB := i + + // look for link end: ' " ) + findlinkend: + for i < len(data) { + switch { + case data[i] == '\\': + i += 2 + + case data[i] == ')' || data[i] == '\'' || data[i] == '"': + break findlinkend + + default: + i++ + } + } + + if i >= len(data) { + return 0, nil + } + linkE := i + + // look for title end if present + titleB, titleE := 0, 0 + if data[i] == '\'' || data[i] == '"' { + i++ + titleB = i + + findtitleend: + for i < len(data) { + switch { + case data[i] == '\\': + i += 2 + + case data[i] == ')': + break findtitleend + + default: + i++ + } + } + + if i >= len(data) { + return 0, nil + } + + // skip whitespace after title + titleE = i - 1 + for titleE > titleB && isspace(data[titleE]) { + titleE-- + } + + // check for closing quote presence + if data[titleE] != '\'' && data[titleE] != '"' { + titleB, titleE = 0, 0 + linkE = i + } + } + + // remove whitespace at the end of the link + for linkE > linkB && isspace(data[linkE-1]) { + linkE-- + } + + // remove optional angle brackets around the link + if data[linkB] == '<' { + linkB++ + } + if data[linkE-1] == '>' { + linkE-- + } + + // build escaped link and title + if linkE > linkB { + link = data[linkB:linkE] + } + + if titleE > titleB { + title = data[titleB:titleE] + } + + i++ + + // reference style link + case isReferenceStyleLink(data, i, t): + var id []byte + altContentConsidered := false + + // look for the id + i++ + linkB := i + for i < len(data) && data[i] != ']' { + i++ + } + if i >= len(data) { + return 0, nil + } + linkE := i + + // find the reference + if linkB == linkE { + if textHasNl { + var b bytes.Buffer + + for j := 1; j < txtE; j++ { + switch { + case data[j] != '\n': + b.WriteByte(data[j]) + case data[j-1] != ' ': + b.WriteByte(' ') + } + } + + id = b.Bytes() + } else { + id = data[1:txtE] + altContentConsidered = true + } + } else { + id = data[linkB:linkE] + } + + // find the reference with matching id + lr, ok := p.getRef(string(id)) + if !ok { + return 0, nil + } + + // keep link and title from reference + link = lr.link + title = lr.title + if altContentConsidered { + altContent = lr.text + } + i++ + + // shortcut reference style link or reference or inline footnote + default: + var id []byte + + // craft the id + if textHasNl { + var b bytes.Buffer + + for j := 1; j < txtE; j++ { + switch { + case data[j] != '\n': + b.WriteByte(data[j]) + case data[j-1] != ' ': + b.WriteByte(' ') + } + } + + id = b.Bytes() + } else { + if t == linkDeferredFootnote { + id = data[2:txtE] // get rid of the ^ + } else { + id = data[1:txtE] + } + } + + footnoteNode = NewNode(Item) + if t == linkInlineFootnote { + // create a new reference + noteID = len(p.notes) + 1 + + var fragment []byte + if len(id) > 0 { + if len(id) < 16 { + fragment = make([]byte, len(id)) + } else { + fragment = make([]byte, 16) + } + copy(fragment, slugify(id)) + } else { + fragment = append([]byte("footnote-"), []byte(strconv.Itoa(noteID))...) + } + + ref := &reference{ + noteID: noteID, + hasBlock: false, + link: fragment, + title: id, + footnote: footnoteNode, + } + + p.notes = append(p.notes, ref) + + link = ref.link + title = ref.title + } else { + // find the reference with matching id + lr, ok := p.getRef(string(id)) + if !ok { + return 0, nil + } + + if t == linkDeferredFootnote { + lr.noteID = len(p.notes) + 1 + lr.footnote = footnoteNode + p.notes = append(p.notes, lr) + } + + // keep link and title from reference + link = lr.link + // if inline footnote, title == footnote contents + title = lr.title + noteID = lr.noteID + } + + // rewind the whitespace + i = txtE + 1 + } + + var uLink []byte + if t == linkNormal || t == linkImg { + if len(link) > 0 { + var uLinkBuf bytes.Buffer + unescapeText(&uLinkBuf, link) + uLink = uLinkBuf.Bytes() + } + + // links need something to click on and somewhere to go + if len(uLink) == 0 || (t == linkNormal && txtE <= 1) { + return 0, nil + } + } + + // call the relevant rendering function + var linkNode *Node + switch t { + case linkNormal: + linkNode = NewNode(Link) + linkNode.Destination = normalizeURI(uLink) + linkNode.Title = title + if len(altContent) > 0 { + linkNode.AppendChild(text(altContent)) + } else { + // links cannot contain other links, so turn off link parsing + // temporarily and recurse + insideLink := p.insideLink + p.insideLink = true + p.inline(linkNode, data[1:txtE]) + p.insideLink = insideLink + } + + case linkImg: + linkNode = NewNode(Image) + linkNode.Destination = uLink + linkNode.Title = title + linkNode.AppendChild(text(data[1:txtE])) + i++ + + case linkInlineFootnote, linkDeferredFootnote: + linkNode = NewNode(Link) + linkNode.Destination = link + linkNode.Title = title + linkNode.NoteID = noteID + linkNode.Footnote = footnoteNode + if t == linkInlineFootnote { + i++ + } + + default: + return 0, nil + } + + return i, linkNode +} + +func (p *Markdown) inlineHTMLComment(data []byte) int { + if len(data) < 5 { + return 0 + } + if data[0] != '<' || data[1] != '!' || data[2] != '-' || data[3] != '-' { + return 0 + } + i := 5 + // scan for an end-of-comment marker, across lines if necessary + for i < len(data) && !(data[i-2] == '-' && data[i-1] == '-' && data[i] == '>') { + i++ + } + // no end-of-comment marker + if i >= len(data) { + return 0 + } + return i + 1 +} + +func stripMailto(link []byte) []byte { + if bytes.HasPrefix(link, []byte("mailto://")) { + return link[9:] + } else if bytes.HasPrefix(link, []byte("mailto:")) { + return link[7:] + } else { + return link + } +} + +// autolinkType specifies a kind of autolink that gets detected. +type autolinkType int + +// These are the possible flag values for the autolink renderer. +const ( + notAutolink autolinkType = iota + normalAutolink + emailAutolink +) + +// '<' when tags or autolinks are allowed +func leftAngle(p *Markdown, data []byte, offset int) (int, *Node) { + data = data[offset:] + altype, end := tagLength(data) + if size := p.inlineHTMLComment(data); size > 0 { + end = size + } + if end > 2 { + if altype != notAutolink { + var uLink bytes.Buffer + unescapeText(&uLink, data[1:end+1-2]) + if uLink.Len() > 0 { + link := uLink.Bytes() + node := NewNode(Link) + node.Destination = link + if altype == emailAutolink { + node.Destination = append([]byte("mailto:"), link...) + } + node.AppendChild(text(stripMailto(link))) + return end, node + } + } else { + htmlTag := NewNode(HTMLSpan) + htmlTag.Literal = data[:end] + return end, htmlTag + } + } + + return end, nil +} + +// '\\' backslash escape +var escapeChars = []byte("\\`*_{}[]()#+-.!:|&<>~") + +func escape(p *Markdown, data []byte, offset int) (int, *Node) { + data = data[offset:] + + if len(data) > 1 { + if p.extensions&BackslashLineBreak != 0 && data[1] == '\n' { + return 2, NewNode(Hardbreak) + } + if bytes.IndexByte(escapeChars, data[1]) < 0 { + return 0, nil + } + + return 2, text(data[1:2]) + } + + return 2, nil +} + +func unescapeText(ob *bytes.Buffer, src []byte) { + i := 0 + for i < len(src) { + org := i + for i < len(src) && src[i] != '\\' { + i++ + } + + if i > org { + ob.Write(src[org:i]) + } + + if i+1 >= len(src) { + break + } + + ob.WriteByte(src[i+1]) + i += 2 + } +} + +// '&' escaped when it doesn't belong to an entity +// valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; +func entity(p *Markdown, data []byte, offset int) (int, *Node) { + data = data[offset:] + + end := 1 + + if end < len(data) && data[end] == '#' { + end++ + } + + for end < len(data) && isalnum(data[end]) { + end++ + } + + if end < len(data) && data[end] == ';' { + end++ // real entity + } else { + return 0, nil // lone '&' + } + + ent := data[:end] + // undo & escaping or it will be converted to &amp; by another + // escaper in the renderer + if bytes.Equal(ent, []byte("&")) { + ent = []byte{'&'} + } + + return end, text(ent) +} + +func linkEndsWithEntity(data []byte, linkEnd int) bool { + entityRanges := htmlEntityRe.FindAllIndex(data[:linkEnd], -1) + return entityRanges != nil && entityRanges[len(entityRanges)-1][1] == linkEnd +} + +// hasPrefixCaseInsensitive is a custom implementation of +// strings.HasPrefix(strings.ToLower(s), prefix) +// we rolled our own because ToLower pulls in a huge machinery of lowercasing +// anything from Unicode and that's very slow. Since this func will only be +// used on ASCII protocol prefixes, we can take shortcuts. +func hasPrefixCaseInsensitive(s, prefix []byte) bool { + if len(s) < len(prefix) { + return false + } + delta := byte('a' - 'A') + for i, b := range prefix { + if b != s[i] && b != s[i]+delta { + return false + } + } + return true +} + +var protocolPrefixes = [][]byte{ + []byte("http://"), + []byte("https://"), + []byte("ftp://"), + []byte("file://"), + []byte("mailto:"), +} + +const shortestPrefix = 6 // len("ftp://"), the shortest of the above + +func maybeAutoLink(p *Markdown, data []byte, offset int) (int, *Node) { + // quick check to rule out most false hits + if p.insideLink || len(data) < offset+shortestPrefix { + return 0, nil + } + for _, prefix := range protocolPrefixes { + endOfHead := offset + 8 // 8 is the len() of the longest prefix + if endOfHead > len(data) { + endOfHead = len(data) + } + if hasPrefixCaseInsensitive(data[offset:endOfHead], prefix) { + return autoLink(p, data, offset) + } + } + return 0, nil +} + +func autoLink(p *Markdown, data []byte, offset int) (int, *Node) { + // Now a more expensive check to see if we're not inside an anchor element + anchorStart := offset + offsetFromAnchor := 0 + for anchorStart > 0 && data[anchorStart] != '<' { + anchorStart-- + offsetFromAnchor++ + } + + anchorStr := anchorRe.Find(data[anchorStart:]) + if anchorStr != nil { + anchorClose := NewNode(HTMLSpan) + anchorClose.Literal = anchorStr[offsetFromAnchor:] + return len(anchorStr) - offsetFromAnchor, anchorClose + } + + // scan backward for a word boundary + rewind := 0 + for offset-rewind > 0 && rewind <= 7 && isletter(data[offset-rewind-1]) { + rewind++ + } + if rewind > 6 { // longest supported protocol is "mailto" which has 6 letters + return 0, nil + } + + origData := data + data = data[offset-rewind:] + + if !isSafeLink(data) { + return 0, nil + } + + linkEnd := 0 + for linkEnd < len(data) && !isEndOfLink(data[linkEnd]) { + linkEnd++ + } + + // Skip punctuation at the end of the link + if (data[linkEnd-1] == '.' || data[linkEnd-1] == ',') && data[linkEnd-2] != '\\' { + linkEnd-- + } + + // But don't skip semicolon if it's a part of escaped entity: + if data[linkEnd-1] == ';' && data[linkEnd-2] != '\\' && !linkEndsWithEntity(data, linkEnd) { + linkEnd-- + } + + // See if the link finishes with a punctuation sign that can be closed. + var copen byte + switch data[linkEnd-1] { + case '"': + copen = '"' + case '\'': + copen = '\'' + case ')': + copen = '(' + case ']': + copen = '[' + case '}': + copen = '{' + default: + copen = 0 + } + + if copen != 0 { + bufEnd := offset - rewind + linkEnd - 2 + + openDelim := 1 + + /* Try to close the final punctuation sign in this same line; + * if we managed to close it outside of the URL, that means that it's + * not part of the URL. If it closes inside the URL, that means it + * is part of the URL. + * + * Examples: + * + * foo http://www.pokemon.com/Pikachu_(Electric) bar + * => http://www.pokemon.com/Pikachu_(Electric) + * + * foo (http://www.pokemon.com/Pikachu_(Electric)) bar + * => http://www.pokemon.com/Pikachu_(Electric) + * + * foo http://www.pokemon.com/Pikachu_(Electric)) bar + * => http://www.pokemon.com/Pikachu_(Electric)) + * + * (foo http://www.pokemon.com/Pikachu_(Electric)) bar + * => foo http://www.pokemon.com/Pikachu_(Electric) + */ + + for bufEnd >= 0 && origData[bufEnd] != '\n' && openDelim != 0 { + if origData[bufEnd] == data[linkEnd-1] { + openDelim++ + } + + if origData[bufEnd] == copen { + openDelim-- + } + + bufEnd-- + } + + if openDelim == 0 { + linkEnd-- + } + } + + var uLink bytes.Buffer + unescapeText(&uLink, data[:linkEnd]) + + if uLink.Len() > 0 { + node := NewNode(Link) + node.Destination = uLink.Bytes() + node.AppendChild(text(uLink.Bytes())) + return linkEnd, node + } + + return linkEnd, nil +} + +func isEndOfLink(char byte) bool { + return isspace(char) || char == '<' +} + +var validUris = [][]byte{[]byte("http://"), []byte("https://"), []byte("ftp://"), []byte("mailto://")} +var validPaths = [][]byte{[]byte("/"), []byte("./"), []byte("../")} + +func isSafeLink(link []byte) bool { + for _, path := range validPaths { + if len(link) >= len(path) && bytes.Equal(link[:len(path)], path) { + if len(link) == len(path) { + return true + } else if isalnum(link[len(path)]) { + return true + } + } + } + + for _, prefix := range validUris { + // TODO: handle unicode here + // case-insensitive prefix test + if len(link) > len(prefix) && bytes.Equal(bytes.ToLower(link[:len(prefix)]), prefix) && isalnum(link[len(prefix)]) { + return true + } + } + + return false +} + +// return the length of the given tag, or 0 is it's not valid +func tagLength(data []byte) (autolink autolinkType, end int) { + var i, j int + + // a valid tag can't be shorter than 3 chars + if len(data) < 3 { + return notAutolink, 0 + } + + // begins with a '<' optionally followed by '/', followed by letter or number + if data[0] != '<' { + return notAutolink, 0 + } + if data[1] == '/' { + i = 2 + } else { + i = 1 + } + + if !isalnum(data[i]) { + return notAutolink, 0 + } + + // scheme test + autolink = notAutolink + + // try to find the beginning of an URI + for i < len(data) && (isalnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-') { + i++ + } + + if i > 1 && i < len(data) && data[i] == '@' { + if j = isMailtoAutoLink(data[i:]); j != 0 { + return emailAutolink, i + j + } + } + + if i > 2 && i < len(data) && data[i] == ':' { + autolink = normalAutolink + i++ + } + + // complete autolink test: no whitespace or ' or " + switch { + case i >= len(data): + autolink = notAutolink + case autolink != notAutolink: + j = i + + for i < len(data) { + if data[i] == '\\' { + i += 2 + } else if data[i] == '>' || data[i] == '\'' || data[i] == '"' || isspace(data[i]) { + break + } else { + i++ + } + + } + + if i >= len(data) { + return autolink, 0 + } + if i > j && data[i] == '>' { + return autolink, i + 1 + } + + // one of the forbidden chars has been found + autolink = notAutolink + } + i += bytes.IndexByte(data[i:], '>') + if i < 0 { + return autolink, 0 + } + return autolink, i + 1 +} + +// look for the address part of a mail autolink and '>' +// this is less strict than the original markdown e-mail address matching +func isMailtoAutoLink(data []byte) int { + nb := 0 + + // address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@' + for i := 0; i < len(data); i++ { + if isalnum(data[i]) { + continue + } + + switch data[i] { + case '@': + nb++ + + case '-', '.', '_': + break + + case '>': + if nb == 1 { + return i + 1 + } + return 0 + default: + return 0 + } + } + + return 0 +} + +// look for the next emph char, skipping other constructs +func helperFindEmphChar(data []byte, c byte) int { + i := 0 + + for i < len(data) { + for i < len(data) && data[i] != c && data[i] != '`' && data[i] != '[' { + i++ + } + if i >= len(data) { + return 0 + } + // do not count escaped chars + if i != 0 && data[i-1] == '\\' { + i++ + continue + } + if data[i] == c { + return i + } + + if data[i] == '`' { + // skip a code span + tmpI := 0 + i++ + for i < len(data) && data[i] != '`' { + if tmpI == 0 && data[i] == c { + tmpI = i + } + i++ + } + if i >= len(data) { + return tmpI + } + i++ + } else if data[i] == '[' { + // skip a link + tmpI := 0 + i++ + for i < len(data) && data[i] != ']' { + if tmpI == 0 && data[i] == c { + tmpI = i + } + i++ + } + i++ + for i < len(data) && (data[i] == ' ' || data[i] == '\n') { + i++ + } + if i >= len(data) { + return tmpI + } + if data[i] != '[' && data[i] != '(' { // not a link + if tmpI > 0 { + return tmpI + } + continue + } + cc := data[i] + i++ + for i < len(data) && data[i] != cc { + if tmpI == 0 && data[i] == c { + return i + } + i++ + } + if i >= len(data) { + return tmpI + } + i++ + } + } + return 0 +} + +func helperEmphasis(p *Markdown, data []byte, c byte) (int, *Node) { + i := 0 + + // skip one symbol if coming from emph3 + if len(data) > 1 && data[0] == c && data[1] == c { + i = 1 + } + + for i < len(data) { + length := helperFindEmphChar(data[i:], c) + if length == 0 { + return 0, nil + } + i += length + if i >= len(data) { + return 0, nil + } + + if i+1 < len(data) && data[i+1] == c { + i++ + continue + } + + if data[i] == c && !isspace(data[i-1]) { + + if p.extensions&NoIntraEmphasis != 0 { + if !(i+1 == len(data) || isspace(data[i+1]) || ispunct(data[i+1])) { + continue + } + } + + emph := NewNode(Emph) + p.inline(emph, data[:i]) + return i + 1, emph + } + } + + return 0, nil +} + +func helperDoubleEmphasis(p *Markdown, data []byte, c byte) (int, *Node) { + i := 0 + + for i < len(data) { + length := helperFindEmphChar(data[i:], c) + if length == 0 { + return 0, nil + } + i += length + + if i+1 < len(data) && data[i] == c && data[i+1] == c && i > 0 && !isspace(data[i-1]) { + nodeType := Strong + if c == '~' { + nodeType = Del + } + node := NewNode(nodeType) + p.inline(node, data[:i]) + return i + 2, node + } + i++ + } + return 0, nil +} + +func helperTripleEmphasis(p *Markdown, data []byte, offset int, c byte) (int, *Node) { + i := 0 + origData := data + data = data[offset:] + + for i < len(data) { + length := helperFindEmphChar(data[i:], c) + if length == 0 { + return 0, nil + } + i += length + + // skip whitespace preceded symbols + if data[i] != c || isspace(data[i-1]) { + continue + } + + switch { + case i+2 < len(data) && data[i+1] == c && data[i+2] == c: + // triple symbol found + strong := NewNode(Strong) + em := NewNode(Emph) + strong.AppendChild(em) + p.inline(em, data[:i]) + return i + 3, strong + case (i+1 < len(data) && data[i+1] == c): + // double symbol found, hand over to emph1 + length, node := helperEmphasis(p, origData[offset-2:], c) + if length == 0 { + return 0, nil + } + return length - 2, node + default: + // single symbol found, hand over to emph2 + length, node := helperDoubleEmphasis(p, origData[offset-1:], c) + if length == 0 { + return 0, nil + } + return length - 1, node + } + } + return 0, nil +} + +func text(s []byte) *Node { + node := NewNode(Text) + node.Literal = s + return node +} + +func normalizeURI(s []byte) []byte { + return s // TODO: implement +} diff --git a/pkg/blackfriday/inline_test.go b/pkg/blackfriday/inline_test.go new file mode 100644 index 00000000..966dc22f --- /dev/null +++ b/pkg/blackfriday/inline_test.go @@ -0,0 +1,1176 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// Unit tests for inline parsing +// + +package blackfriday + +import ( + "regexp" + "testing" + + "strings" +) + +func TestEmphasis(t *testing.T) { + var tests = []string{ + "nothing inline\n", + "

    nothing inline

    \n", + + "simple *inline* test\n", + "

    simple inline test

    \n", + + "*at the* beginning\n", + "

    at the beginning

    \n", + + "at the *end*\n", + "

    at the end

    \n", + + "*try two* in *one line*\n", + "

    try two in one line

    \n", + + "over *two\nlines* test\n", + "

    over two\nlines test

    \n", + + "odd *number of* markers* here\n", + "

    odd number of markers* here

    \n", + + "odd *number\nof* markers* here\n", + "

    odd number\nof markers* here

    \n", + + "simple _inline_ test\n", + "

    simple inline test

    \n", + + "_at the_ beginning\n", + "

    at the beginning

    \n", + + "at the _end_\n", + "

    at the end

    \n", + + "_try two_ in _one line_\n", + "

    try two in one line

    \n", + + "over _two\nlines_ test\n", + "

    over two\nlines test

    \n", + + "odd _number of_ markers_ here\n", + "

    odd number of markers_ here

    \n", + + "odd _number\nof_ markers_ here\n", + "

    odd number\nof markers_ here

    \n", + + "mix of *markers_\n", + "

    mix of *markers_

    \n", + + "*What is A\\* algorithm?*\n", + "

    What is A* algorithm?

    \n", + } + doTestsInline(t, tests) +} + +func TestReferenceOverride(t *testing.T) { + var tests = []string{ + "test [ref1][]\n", + "

    test ref1

    \n", + + "test [my ref][ref1]\n", + "

    test my ref

    \n", + + "test [ref2][]\n\n[ref2]: http://www.leftalone.com/ (Ref left alone)\n", + "

    test ref2

    \n", + + "test [ref3][]\n\n[ref3]: http://www.leftalone.com/ (Ref left alone)\n", + "

    test ref3

    \n", + + "test [ref4][]\n\n[ref4]: http://zombo.com/ (You can do anything)\n", + "

    test [ref4][]

    \n", + + "test [!(*http.ServeMux).ServeHTTP][] complicated ref\n", + "

    test !(*http.ServeMux).ServeHTTP complicated ref

    \n", + + "test [ref5][]\n", + "

    test Moo

    \n", + } + doTestsInlineParam(t, tests, TestParams{ + referenceOverride: func(reference string) (rv *Reference, overridden bool) { + switch reference { + case "ref1": + // just an overridden reference exists without definition + return &Reference{ + Link: "http://www.ref1.com/", + Title: "Reference 1"}, true + case "ref2": + // overridden exists and reference defined + return &Reference{ + Link: "http://www.overridden.com/", + Title: "Reference Overridden"}, true + case "ref3": + // not overridden and reference defined + return nil, false + case "ref4": + // overridden missing and defined + return nil, true + case "!(*http.ServeMux).ServeHTTP": + return &Reference{ + Link: "http://localhost:6060/pkg/net/http/#ServeMux.ServeHTTP", + Title: "ServeHTTP docs"}, true + case "ref5": + return &Reference{ + Link: "http://www.ref5.com/", + Title: "Reference 5", + Text: "Moo", + }, true + } + return nil, false + }, + }) +} + +func TestStrong(t *testing.T) { + var tests = []string{ + "nothing inline\n", + "

    nothing inline

    \n", + + "simple **inline** test\n", + "

    simple inline test

    \n", + + "**at the** beginning\n", + "

    at the beginning

    \n", + + "at the **end**\n", + "

    at the end

    \n", + + "**try two** in **one line**\n", + "

    try two in one line

    \n", + + "over **two\nlines** test\n", + "

    over two\nlines test

    \n", + + "odd **number of** markers** here\n", + "

    odd number of markers** here

    \n", + + "odd **number\nof** markers** here\n", + "

    odd number\nof markers** here

    \n", + + "simple __inline__ test\n", + "

    simple inline test

    \n", + + "__at the__ beginning\n", + "

    at the beginning

    \n", + + "at the __end__\n", + "

    at the end

    \n", + + "__try two__ in __one line__\n", + "

    try two in one line

    \n", + + "over __two\nlines__ test\n", + "

    over two\nlines test

    \n", + + "odd __number of__ markers__ here\n", + "

    odd number of markers__ here

    \n", + + "odd __number\nof__ markers__ here\n", + "

    odd number\nof markers__ here

    \n", + + "mix of **markers__\n", + "

    mix of **markers__

    \n", + + "**`/usr`** : this folder is named `usr`\n", + "

    /usr : this folder is named usr

    \n", + + "**`/usr`** :\n\n this folder is named `usr`\n", + "

    /usr :

    \n\n

    this folder is named usr

    \n", + } + doTestsInline(t, tests) +} + +func TestEmphasisMix(t *testing.T) { + var tests = []string{ + "***triple emphasis***\n", + "

    triple emphasis

    \n", + + "***triple\nemphasis***\n", + "

    triple\nemphasis

    \n", + + "___triple emphasis___\n", + "

    triple emphasis

    \n", + + "***triple emphasis___\n", + "

    ***triple emphasis___

    \n", + + "*__triple emphasis__*\n", + "

    triple emphasis

    \n", + + "__*triple emphasis*__\n", + "

    triple emphasis

    \n", + + "**improper *nesting** is* bad\n", + "

    improper *nesting is* bad

    \n", + + "*improper **nesting* is** bad\n", + "

    *improper nesting* is bad

    \n", + } + doTestsInline(t, tests) +} + +func TestEmphasisLink(t *testing.T) { + var tests = []string{ + "[first](before) *text[second] (inside)text* [third](after)\n", + "

    first textsecondtext third

    \n", + + "*incomplete [link] definition*\n", + "

    incomplete [link] definition

    \n", + + "*it's [emphasis*] (not link)\n", + "

    it's [emphasis] (not link)

    \n", + + "*it's [emphasis*] and *[asterisk]\n", + "

    it's [emphasis] and *[asterisk]

    \n", + } + doTestsInline(t, tests) +} + +func TestStrikeThrough(t *testing.T) { + var tests = []string{ + "nothing inline\n", + "

    nothing inline

    \n", + + "simple ~~inline~~ test\n", + "

    simple inline test

    \n", + + "~~at the~~ beginning\n", + "

    at the beginning

    \n", + + "at the ~~end~~\n", + "

    at the end

    \n", + + "~~try two~~ in ~~one line~~\n", + "

    try two in one line

    \n", + + "over ~~two\nlines~~ test\n", + "

    over two\nlines test

    \n", + + "odd ~~number of~~ markers~~ here\n", + "

    odd number of markers~~ here

    \n", + + "odd ~~number\nof~~ markers~~ here\n", + "

    odd number\nof markers~~ here

    \n", + } + doTestsInline(t, tests) +} + +func TestCodeSpan(t *testing.T) { + var tests = []string{ + "`source code`\n", + "

    source code

    \n", + + "` source code with spaces `\n", + "

    source code with spaces

    \n", + + "` source code with spaces `not here\n", + "

    source code with spacesnot here

    \n", + + "a `single marker\n", + "

    a `single marker

    \n", + + "a single multi-tick marker with ``` no text\n", + "

    a single multi-tick marker with ``` no text

    \n", + + "markers with ` ` a space\n", + "

    markers with a space

    \n", + + "`source code` and a `stray\n", + "

    source code and a `stray

    \n", + + "`source *with* _awkward characters_ in it`\n", + "

    source *with* _awkward characters_ in it

    \n", + + "`split over\ntwo lines`\n", + "

    split over\ntwo lines

    \n", + + "```multiple ticks``` for the marker\n", + "

    multiple ticks for the marker

    \n", + + "```multiple ticks `with` ticks inside```\n", + "

    multiple ticks `with` ticks inside

    \n", + } + doTestsInline(t, tests) +} + +func TestLineBreak(t *testing.T) { + var tests = []string{ + "this line \nhas a break\n", + "

    this line
    \nhas a break

    \n", + + "this line \ndoes not\n", + "

    this line\ndoes not

    \n", + + "this line\\\ndoes not\n", + "

    this line\\\ndoes not

    \n", + + "this line\\ \ndoes not\n", + "

    this line\\\ndoes not

    \n", + + "this has an \nextra space\n", + "

    this has an
    \nextra space

    \n", + } + doTestsInline(t, tests) + + tests = []string{ + "this line \nhas a break\n", + "

    this line
    \nhas a break

    \n", + + "this line \ndoes not\n", + "

    this line\ndoes not

    \n", + + "this line\\\nhas a break\n", + "

    this line
    \nhas a break

    \n", + + "this line\\ \ndoes not\n", + "

    this line\\\ndoes not

    \n", + + "this has an \nextra space\n", + "

    this has an
    \nextra space

    \n", + } + doTestsInlineParam(t, tests, TestParams{ + extensions: BackslashLineBreak}) +} + +func TestInlineLink(t *testing.T) { + var tests = []string{ + "[foo](/bar/)\n", + "

    foo

    \n", + + "[foo with a title](/bar/ \"title\")\n", + "

    foo with a title

    \n", + + "[foo with a title](/bar/\t\"title\")\n", + "

    foo with a title

    \n", + + "[foo with a title](/bar/ \"title\" )\n", + "

    foo with a title

    \n", + + "[foo with a title](/bar/ title with no quotes)\n", + "

    foo with a title

    \n", + + "[foo]()\n", + "

    [foo]()

    \n", + + "![foo](/bar/)\n", + "

    \"foo\"

    \n", + + "![foo with a title](/bar/ \"title\")\n", + "

    \"foo

    \n", + + "![foo with a title](/bar/\t\"title\")\n", + "

    \"foo

    \n", + + "![foo with a title](/bar/ \"title\" )\n", + "

    \"foo

    \n", + + "![foo with a title](/bar/ title with no quotes)\n", + "

    \"foo

    \n", + + "![](img.jpg)\n", + "

    \"\"

    \n", + + "[link](url)\n", + "

    link

    \n", + + "![foo]()\n", + "

    ![foo]()

    \n", + + "[a link]\t(/with_a_tab/)\n", + "

    a link

    \n", + + "[a link] (/with_spaces/)\n", + "

    a link

    \n", + + "[text (with) [[nested] (brackets)]](/url/)\n", + "

    text (with) [[nested] (brackets)]

    \n", + + "[text (with) [broken nested] (brackets)]](/url/)\n", + "

    [text (with) broken nested]](/url/)

    \n", + + "[text\nwith a newline](/link/)\n", + "

    text\nwith a newline

    \n", + + "[text in brackets] [followed](/by a link/)\n", + "

    [text in brackets] followed

    \n", + + "[link with\\] a closing bracket](/url/)\n", + "

    link with] a closing bracket

    \n", + + "[link with\\[ an opening bracket](/url/)\n", + "

    link with[ an opening bracket

    \n", + + "[link with\\) a closing paren](/url/)\n", + "

    link with) a closing paren

    \n", + + "[link with\\( an opening paren](/url/)\n", + "

    link with( an opening paren

    \n", + + "[link]( with whitespace)\n", + "

    link

    \n", + + "[link]( with whitespace )\n", + "

    link

    \n", + + "[![image](someimage)](with image)\n", + "

    \"image\"

    \n", + + "[link](url \"one quote)\n", + "

    link

    \n", + + "[link](url 'one quote)\n", + "

    link

    \n", + + "[link]()\n", + "

    link

    \n", + + "[link & ampersand](/url/)\n", + "

    link & ampersand

    \n", + + "[link & ampersand](/url/)\n", + "

    link & ampersand

    \n", + + "[link](/url/&query)\n", + "

    link

    \n", + + "[[t]](/t)\n", + "

    [t]

    \n", + + "[link]()\n", + "

    link

    \n", + + "[link](<./>)\n", + "

    link

    \n", + + "[link](<../>)\n", + "

    link

    \n", + } + doLinkTestsInline(t, tests) + +} + +func TestRelAttrLink(t *testing.T) { + var nofollowTests = []string{ + "[foo](http://bar.com/foo/)\n", + "

    foo

    \n", + + "[foo](/bar/)\n", + "

    foo

    \n", + + "[foo](/)\n", + "

    foo

    \n", + + "[foo](./)\n", + "

    foo

    \n", + + "[foo](../)\n", + "

    foo

    \n", + + "[foo](../bar)\n", + "

    foo

    \n", + } + doTestsInlineParam(t, nofollowTests, TestParams{ + HTMLFlags: Safelink | NofollowLinks, + }) + + var noreferrerTests = []string{ + "[foo](http://bar.com/foo/)\n", + "

    foo

    \n", + + "[foo](/bar/)\n", + "

    foo

    \n", + } + doTestsInlineParam(t, noreferrerTests, TestParams{ + HTMLFlags: Safelink | NoreferrerLinks, + }) + + var nofollownoreferrerTests = []string{ + "[foo](http://bar.com/foo/)\n", + "

    foo

    \n", + + "[foo](/bar/)\n", + "

    foo

    \n", + } + doTestsInlineParam(t, nofollownoreferrerTests, TestParams{ + HTMLFlags: Safelink | NofollowLinks | NoreferrerLinks, + }) +} + +func TestHrefTargetBlank(t *testing.T) { + var tests = []string{ + // internal link + "[foo](/bar/)\n", + "

    foo

    \n", + + "[foo](/)\n", + "

    foo

    \n", + + "[foo](./)\n", + "

    foo

    \n", + + "[foo](./bar)\n", + "

    foo

    \n", + + "[foo](../)\n", + "

    foo

    \n", + + "[foo](../bar)\n", + "

    foo

    \n", + + "[foo](http://example.com)\n", + "

    foo

    \n", + } + doTestsInlineParam(t, tests, TestParams{ + HTMLFlags: Safelink | HrefTargetBlank, + }) +} + +func TestSafeInlineLink(t *testing.T) { + var tests = []string{ + "[foo](/bar/)\n", + "

    foo

    \n", + + "[foo](/)\n", + "

    foo

    \n", + + "[foo](./)\n", + "

    foo

    \n", + + "[foo](../)\n", + "

    foo

    \n", + + "[foo](http://bar/)\n", + "

    foo

    \n", + + "[foo](https://bar/)\n", + "

    foo

    \n", + + "[foo](ftp://bar/)\n", + "

    foo

    \n", + + "[foo](mailto://bar/)\n", + "

    foo

    \n", + + // Not considered safe + "[foo](baz://bar/)\n", + "

    foo

    \n", + } + doSafeTestsInline(t, tests) +} + +func TestReferenceLink(t *testing.T) { + var tests = []string{ + "[link][ref]\n", + "

    [link][ref]

    \n", + + "[link][ref]\n [ref]: /url/ \"title\"\n", + "

    link

    \n", + + "[link][ref]\n [ref]: /url/\n", + "

    link

    \n", + + " [ref]: /url/\n", + "", + + " [ref]: /url/\n[ref2]: /url/\n [ref3]: /url/\n", + "", + + " [ref]: /url/\n[ref2]: /url/\n [ref3]: /url/\n [4spaces]: /url/\n", + "
    [4spaces]: /url/\n
    \n", + + "[hmm](ref2)\n [ref]: /url/\n[ref2]: /url/\n [ref3]: /url/\n", + "

    hmm

    \n", + + "[ref]\n", + "

    [ref]

    \n", + + "[ref]\n [ref]: /url/ \"title\"\n", + "

    ref

    \n", + + "[ref]\n [ref]: ../url/ \"title\"\n", + "

    ref

    \n", + + "[link][ref]\n [ref]: /url/", + "

    link

    \n", + } + doLinkTestsInline(t, tests) +} + +func TestTags(t *testing.T) { + var tests = []string{ + "a tag\n", + "

    a tag

    \n", + + "tag\n", + "

    tag

    \n", + + "mismatch\n", + "

    mismatch

    \n", + + "a tag\n", + "

    a tag

    \n", + } + doTestsInline(t, tests) +} + +func TestAutoLink(t *testing.T) { + var tests = []string{ + "http://foo.com/\n", + "

    http://foo.com/

    \n", + + "1 http://foo.com/\n", + "

    1 http://foo.com/

    \n", + + "1http://foo.com/\n", + "

    1http://foo.com/

    \n", + + "1.http://foo.com/\n", + "

    1.http://foo.com/

    \n", + + "1. http://foo.com/\n", + "
      \n
    1. http://foo.com/
    2. \n
    \n", + + "-http://foo.com/\n", + "

    -http://foo.com/

    \n", + + "- http://foo.com/\n", + "\n", + + "_http://foo.com/\n", + "

    _http://foo.com/

    \n", + + "令狐http://foo.com/\n", + "

    令狐http://foo.com/

    \n", + + "令狐 http://foo.com/\n", + "

    令狐 http://foo.com/

    \n", + + "ahttp://foo.com/\n", + "

    ahttp://foo.com/

    \n", + + ">http://foo.com/\n", + "
    \n

    http://foo.com/

    \n
    \n", + + "> http://foo.com/\n", + "
    \n

    http://foo.com/

    \n
    \n", + + "go to \n", + "

    go to http://foo.com/

    \n", + + "a secure \n", + "

    a secure https://link.org

    \n", + + "an email \n", + "

    an email some@one.com

    \n", + + "an email \n", + "

    an email some@one.com

    \n", + + "an email \n", + "

    an email some@one.com

    \n", + + "an ftp \n", + "

    an ftp ftp://old.com

    \n", + + "an ftp \n", + "

    an ftp ftp:old.com

    \n", + + "a link with \n", + "

    a link with " + + "http://new.com?query=foo&bar

    \n", + + "quotes mean a tag \n", + "

    quotes mean a tag

    \n", + + "quotes mean a tag \n", + "

    quotes mean a tag

    \n", + + "unless escaped \n", + "

    unless escaped " + + "http://new.com?query="foo"&bar

    \n", + + "even a > can be escaped &etc>\n", + "

    even a > can be escaped " + + "http://new.com?q=>&etc

    \n", + + "http://fancy.com\n", + "

    http://fancy.com

    \n", + + "This is a link\n", + "

    This is a link

    \n", + + "http://www.fancy.com/A_B.pdf\n", + "

    http://www.fancy.com/A_B.pdf

    \n", + + "(http://www.fancy.com/A_B (\n", + "

    (http://www.fancy.com/A_B (

    \n", + + "(http://www.fancy.com/A_B (part two: http://www.fancy.com/A_B)).\n", + "

    (http://www.fancy.com/A_B (part two: http://www.fancy.com/A_B)).

    \n", + + "http://www.foo.com
    \n", + "

    http://www.foo.com

    \n", + + "http://foo.com/viewtopic.php?f=18&t=297", + "

    http://foo.com/viewtopic.php?f=18&t=297

    \n", + + "http://foo.com/viewtopic.php?param="18"zz", + "

    http://foo.com/viewtopic.php?param="18"zz

    \n", + + "http://foo.com/viewtopic.php?param="18"", + "

    http://foo.com/viewtopic.php?param="18"

    \n", + + "https://fancy.com\n", + "

    https://fancy.com

    \n", + } + doLinkTestsInline(t, tests) +} + +var footnoteTests = []string{ + "testing footnotes.[^a]\n\n[^a]: This is the note\n", + `

    testing footnotes.1

    + +
    + +
    + +
      +
    1. This is the note
    2. +
    + +
    +`, + + `testing long[^b] notes. + +[^b]: Paragraph 1 + + Paragraph 2 + + ` + "```\n\tsome code\n\t```" + ` + + Paragraph 3 + +No longer in the footnote +`, + `

    testing long1 notes.

    + +

    No longer in the footnote

    + +
    + +
    + +
      +
    1. Paragraph 1

      + +

      Paragraph 2

      + +

      +some code +

      + +

      Paragraph 3

    2. +
    + +
    +`, + + `testing[^c] multiple[^d] notes. + +[^c]: this is [note] c + + +omg + +[^d]: this is note d + +what happens here + +[note]: /link/c + +`, + `

    testing1 multiple2 notes.

    + +

    omg

    + +

    what happens here

    + +
    + +
    + +
      +
    1. this is note c
    2. + +
    3. this is note d
    4. +
    + +
    +`, + + "testing inline^[this is the note] notes.\n", + `

    testing inline1 notes.

    + +
    + +
    + +
      +
    1. this is the note
    2. +
    + +
    +`, + + "testing multiple[^1] types^[inline note] of notes[^2]\n\n[^2]: the second deferred note\n[^1]: the first deferred note\n\n\twhich happens to be a block\n", + `

    testing multiple1 types2 of notes3

    + +
    + +
    + +
      +
    1. the first deferred note

      + +

      which happens to be a block

    2. + +
    3. inline note
    4. + +
    5. the second deferred note
    6. +
    + +
    +`, + + `This is a footnote[^1]^[and this is an inline footnote] + +[^1]: the footnote text. + + may be multiple paragraphs. +`, + `

    This is a footnote12

    + +
    + +
    + +
      +
    1. the footnote text.

      + +

      may be multiple paragraphs.

    2. + +
    3. and this is an inline footnote
    4. +
    + +
    +`, + + "empty footnote[^]\n\n[^]: fn text", + "

    empty footnote1

    \n\n
    \n\n
    \n\n
      \n
    1. fn text
    2. \n
    \n\n
    \n", + + "Some text.[^note1]\n\n[^note1]: fn1", + "

    Some text.1

    \n\n
    \n\n
    \n\n
      \n
    1. fn1
    2. \n
    \n\n
    \n", + + "Some text.[^note1][^note2]\n\n[^note1]: fn1\n[^note2]: fn2\n", + "

    Some text.12

    \n\n
    \n\n
    \n\n
      \n
    1. fn1
    2. \n\n
    3. fn2
    4. \n
    \n\n
    \n", + + `Bla bla [^1] [WWW][w3] + +[^1]: This is a footnote + +[w3]: http://www.w3.org/ +`, + `

    Bla bla 1 WWW

    + +
    + +
    + +
      +
    1. This is a footnote
    2. +
    + +
    +`, + + `This is exciting![^fn1] + +[^fn1]: Fine print +`, + `

    This is exciting!1

    + +
    + +
    + +
      +
    1. Fine print
    2. +
    + +
    +`, + + `This text does not reference a footnote. + +[^footnote]: But it has a footnote! And it gets omitted. +`, + "

    This text does not reference a footnote.

    \n", +} + +func TestFootnotes(t *testing.T) { + doTestsInlineParam(t, footnoteTests, TestParams{ + extensions: Footnotes, + }) +} + +func TestFootnotesWithParameters(t *testing.T) { + tests := make([]string, len(footnoteTests)) + + prefix := "testPrefix" + returnText := "ret" + re := regexp.MustCompile(`(?ms)
  • (.*?)
  • `) + + // Transform the test expectations to match the parameters we're using. + for i, test := range footnoteTests { + if i%2 == 1 { + test = strings.Replace(test, "fn:", "fn:"+prefix, -1) + test = strings.Replace(test, "fnref:", "fnref:"+prefix, -1) + test = re.ReplaceAllString(test, `
  • $2 ret
  • `) + } + tests[i] = test + } + + params := HTMLRendererParameters{ + FootnoteAnchorPrefix: prefix, + FootnoteReturnLinkContents: returnText, + } + + doTestsInlineParam(t, tests, TestParams{ + extensions: Footnotes, + HTMLFlags: FootnoteReturnLinks, + HTMLRendererParameters: params, + }) +} + +func TestNestedFootnotes(t *testing.T) { + var tests = []string{ + `Paragraph.[^fn1] + +[^fn1]: + Asterisk[^fn2] + +[^fn2]: + Obelisk`, + `

    Paragraph.1

    + +
    + +
    + +
      +
    1. Asterisk2
    2. + +
    3. Obelisk
    4. +
    + +
    +`, + } + doTestsInlineParam(t, tests, TestParams{extensions: Footnotes}) +} + +func TestInlineComments(t *testing.T) { + var tests = []string{ + "Hello \n", + "

    Hello

    \n", + + "Hello ", + "

    Hello

    \n", + + "Hello \n", + "

    Hello

    \n", + + "Hello \na", + "

    Hello \na

    \n", + + "* list \n", + "
      \n
    • list
    • \n
    \n", + + " comment\n", + "

    comment

    \n", + + "blahblah\n\nrhubarb\n", + "

    blahblah\n\nrhubarb

    \n", + } + doTestsInlineParam(t, tests, TestParams{HTMLFlags: Smartypants | SmartypantsDashes}) +} + +func TestSmartDoubleQuotes(t *testing.T) { + var tests = []string{ + "this should be normal \"quoted\" text.\n", + "

    this should be normal “quoted” text.

    \n", + "this \" single double\n", + "

    this “ single double

    \n", + "two pair of \"some\" quoted \"text\".\n", + "

    two pair of “some” quoted “text”.

    \n"} + + doTestsInlineParam(t, tests, TestParams{HTMLFlags: Smartypants}) +} + +func TestSmartDoubleQuotesNBSP(t *testing.T) { + var tests = []string{ + "this should be normal \"quoted\" text.\n", + "

    this should be normal “ quoted ” text.

    \n", + "this \" single double\n", + "

    this “  single double

    \n", + "two pair of \"some\" quoted \"text\".\n", + "

    two pair of “ some ” quoted “ text ”.

    \n"} + + doTestsInlineParam(t, tests, TestParams{HTMLFlags: Smartypants | SmartypantsQuotesNBSP}) +} + +func TestSmartAngledDoubleQuotes(t *testing.T) { + var tests = []string{ + "this should be angled \"quoted\" text.\n", + "

    this should be angled «quoted» text.

    \n", + "this \" single double\n", + "

    this « single double

    \n", + "two pair of \"some\" quoted \"text\".\n", + "

    two pair of «some» quoted «text».

    \n"} + + doTestsInlineParam(t, tests, TestParams{HTMLFlags: Smartypants | SmartypantsAngledQuotes}) +} + +func TestSmartAngledDoubleQuotesNBSP(t *testing.T) { + var tests = []string{ + "this should be angled \"quoted\" text.\n", + "

    this should be angled « quoted » text.

    \n", + "this \" single double\n", + "

    this «  single double

    \n", + "two pair of \"some\" quoted \"text\".\n", + "

    two pair of « some » quoted « text ».

    \n"} + + doTestsInlineParam(t, tests, TestParams{HTMLFlags: Smartypants | SmartypantsAngledQuotes | SmartypantsQuotesNBSP}) +} + +func TestSmartFractions(t *testing.T) { + var tests = []string{ + "1/2, 1/4 and 3/4; 1/4th and 3/4ths\n", + "

    ½, ¼ and ¾; ¼th and ¾ths

    \n", + "1/2/2015, 1/4/2015, 3/4/2015; 2015/1/2, 2015/1/4, 2015/3/4.\n", + "

    1/2/2015, 1/4/2015, 3/4/2015; 2015/1/2, 2015/1/4, 2015/3/4.

    \n"} + + doTestsInlineParam(t, tests, TestParams{HTMLFlags: Smartypants}) + + tests = []string{ + "1/2, 2/3, 81/100 and 1000000/1048576.\n", + "

    12, 23, 81100 and 10000001048576.

    \n", + "1/2/2015, 1/4/2015, 3/4/2015; 2015/1/2, 2015/1/4, 2015/3/4.\n", + "

    1/2/2015, 1/4/2015, 3/4/2015; 2015/1/2, 2015/1/4, 2015/3/4.

    \n"} + + doTestsInlineParam(t, tests, TestParams{HTMLFlags: Smartypants | SmartypantsFractions}) +} + +func TestDisableSmartDashes(t *testing.T) { + doTestsInlineParam(t, []string{ + "foo - bar\n", + "

    foo - bar

    \n", + "foo -- bar\n", + "

    foo -- bar

    \n", + "foo --- bar\n", + "

    foo --- bar

    \n", + }, TestParams{}) + doTestsInlineParam(t, []string{ + "foo - bar\n", + "

    foo – bar

    \n", + "foo -- bar\n", + "

    foo — bar

    \n", + "foo --- bar\n", + "

    foo —– bar

    \n", + }, TestParams{HTMLFlags: Smartypants | SmartypantsDashes}) + doTestsInlineParam(t, []string{ + "foo - bar\n", + "

    foo - bar

    \n", + "foo -- bar\n", + "

    foo – bar

    \n", + "foo --- bar\n", + "

    foo — bar

    \n", + }, TestParams{HTMLFlags: Smartypants | SmartypantsLatexDashes | SmartypantsDashes}) + doTestsInlineParam(t, []string{ + "foo - bar\n", + "

    foo - bar

    \n", + "foo -- bar\n", + "

    foo -- bar

    \n", + "foo --- bar\n", + "

    foo --- bar

    \n", + }, TestParams{HTMLFlags: Smartypants | SmartypantsLatexDashes}) +} + +func TestSkipLinks(t *testing.T) { + doTestsInlineParam(t, []string{ + "[foo](gopher://foo.bar)", + "

    foo

    \n", + + "[foo](mailto://bar/)\n", + "

    foo

    \n", + }, TestParams{ + HTMLFlags: SkipLinks, + }) +} + +func TestSkipImages(t *testing.T) { + doTestsInlineParam(t, []string{ + "![foo](/bar/)\n", + "

    \n", + }, TestParams{ + HTMLFlags: SkipImages, + }) +} + +func TestUseXHTML(t *testing.T) { + doTestsParam(t, []string{ + "---", + "
    \n", + }, TestParams{}) + doTestsParam(t, []string{ + "---", + "
    \n", + }, TestParams{HTMLFlags: UseXHTML}) +} + +func TestSkipHTML(t *testing.T) { + doTestsParam(t, []string{ + "
    \n\ntext\n\n
    the form
    ", + "

    text

    \n\n

    the form

    \n", + + "text inline html more text", + "

    text inline html more text

    \n", + }, TestParams{HTMLFlags: SkipHTML}) +} + +func BenchmarkSmartDoubleQuotes(b *testing.B) { + params := TestParams{HTMLFlags: Smartypants} + params.extensions |= Autolink | Strikethrough + params.HTMLFlags |= UseXHTML + + for i := 0; i < b.N; i++ { + runMarkdown("this should be normal \"quoted\" text.\n", params) + } +} diff --git a/pkg/blackfriday/markdown.go b/pkg/blackfriday/markdown.go new file mode 100644 index 00000000..1146a105 --- /dev/null +++ b/pkg/blackfriday/markdown.go @@ -0,0 +1,940 @@ +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. + +package blackfriday + +import ( + "bytes" + "fmt" + "io" + "strings" + "unicode/utf8" +) + +// +// Markdown parsing and processing +// + +// Version string of the package. Appears in the rendered document when +// CompletePage flag is on. +const Version = "2.0" + +// Extensions is a bitwise or'ed collection of enabled Blackfriday's +// extensions. +type Extensions int + +// These are the supported markdown parsing extensions. +// OR these values together to select multiple extensions. +const ( + NoExtensions Extensions = 0 + NoIntraEmphasis Extensions = 1 << iota // Ignore emphasis markers inside words + Tables // Render tables + FencedCode // Render fenced code blocks + Autolink // Detect embedded URLs that are not explicitly marked + Strikethrough // Strikethrough text using ~~test~~ + LaxHTMLBlocks // Loosen up HTML block parsing rules + SpaceHeadings // Be strict about prefix heading rules + HardLineBreak // Translate newlines into line breaks + TabSizeEight // Expand tabs to eight spaces instead of four + Footnotes // Pandoc-style footnotes + NoEmptyLineBeforeBlock // No need to insert an empty line to start a (code, quote, ordered list, unordered list) block + HeadingIDs // specify heading IDs with {#id} + Titleblock // Titleblock ala pandoc + AutoHeadingIDs // Create the heading ID from the text + BackslashLineBreak // Translate trailing backslashes into line breaks + DefinitionLists // Render definition lists + + CommonHTMLFlags HTMLFlags = UseXHTML | Smartypants | + SmartypantsFractions | SmartypantsDashes | SmartypantsLatexDashes + + CommonExtensions Extensions = NoIntraEmphasis | Tables | FencedCode | + Autolink | Strikethrough | SpaceHeadings | HeadingIDs | + BackslashLineBreak | DefinitionLists +) + +// ListType contains bitwise or'ed flags for list and list item objects. +type ListType int + +// These are the possible flag values for the ListItem renderer. +// Multiple flag values may be ORed together. +// These are mostly of interest if you are writing a new output format. +const ( + ListTypeOrdered ListType = 1 << iota + ListTypeDefinition + ListTypeTerm + + ListItemContainsBlock + ListItemBeginningOfList // TODO: figure out if this is of any use now + ListItemEndOfList +) + +// CellAlignFlags holds a type of alignment in a table cell. +type CellAlignFlags int + +// These are the possible flag values for the table cell renderer. +// Only a single one of these values will be used; they are not ORed together. +// These are mostly of interest if you are writing a new output format. +const ( + TableAlignmentLeft CellAlignFlags = 1 << iota + TableAlignmentRight + TableAlignmentCenter = (TableAlignmentLeft | TableAlignmentRight) +) + +// The size of a tab stop. +const ( + TabSizeDefault = 4 + TabSizeDouble = 8 +) + +// blockTags is a set of tags that are recognized as HTML block tags. +// Any of these can be included in markdown text without special escaping. +var blockTags = map[string]struct{}{ + "blockquote": {}, + "del": {}, + "div": {}, + "dl": {}, + "fieldset": {}, + "form": {}, + "h1": {}, + "h2": {}, + "h3": {}, + "h4": {}, + "h5": {}, + "h6": {}, + "iframe": {}, + "ins": {}, + "math": {}, + "noscript": {}, + "ol": {}, + "pre": {}, + "p": {}, + "script": {}, + "style": {}, + "table": {}, + "ul": {}, + + // HTML5 + "address": {}, + "article": {}, + "aside": {}, + "canvas": {}, + "figcaption": {}, + "figure": {}, + "footer": {}, + "header": {}, + "hgroup": {}, + "main": {}, + "nav": {}, + "output": {}, + "progress": {}, + "section": {}, + "video": {}, +} + +// Renderer is the rendering interface. This is mostly of interest if you are +// implementing a new rendering format. +// +// Only an HTML implementation is provided in this repository, see the README +// for external implementations. +type Renderer interface { + // RenderNode is the main rendering method. It will be called once for + // every leaf node and twice for every non-leaf node (first with + // entering=true, then with entering=false). The method should write its + // rendition of the node to the supplied writer w. + RenderNode(w io.Writer, node *Node, entering bool) WalkStatus + + // RenderHeader is a method that allows the renderer to produce some + // content preceding the main body of the output document. The header is + // understood in the broad sense here. For example, the default HTML + // renderer will write not only the HTML document preamble, but also the + // table of contents if it was requested. + // + // The method will be passed an entire document tree, in case a particular + // implementation needs to inspect it to produce output. + // + // The output should be written to the supplied writer w. If your + // implementation has no header to write, supply an empty implementation. + RenderHeader(w io.Writer, ast *Node) + + // RenderFooter is a symmetric counterpart of RenderHeader. + RenderFooter(w io.Writer, ast *Node) +} + +// Callback functions for inline parsing. One such function is defined +// for each character that triggers a response when parsing inline data. +type inlineParser func(p *Markdown, data []byte, offset int) (int, *Node) + +// Markdown is a type that holds extensions and the runtime state used by +// Parse, and the renderer. You can not use it directly, construct it with New. +type Markdown struct { + renderer Renderer + referenceOverride ReferenceOverrideFunc + refs map[string]*reference + inlineCallback [256]inlineParser + extensions Extensions + nesting int + maxNesting int + insideLink bool + + // Footnotes need to be ordered as well as available to quickly check for + // presence. If a ref is also a footnote, it's stored both in refs and here + // in notes. Slice is nil if footnotes not enabled. + notes []*reference + + doc *Node + tip *Node // = doc + oldTip *Node + lastMatchedContainer *Node // = doc + allClosed bool +} + +func (p *Markdown) getRef(refid string) (ref *reference, found bool) { + if p.referenceOverride != nil { + r, overridden := p.referenceOverride(refid) + if overridden { + if r == nil { + return nil, false + } + return &reference{ + link: []byte(r.Link), + title: []byte(r.Title), + noteID: 0, + hasBlock: false, + text: []byte(r.Text)}, true + } + } + // refs are case insensitive + ref, found = p.refs[strings.ToLower(refid)] + return ref, found +} + +func (p *Markdown) finalize(block *Node) { + above := block.Parent + block.open = false + p.tip = above +} + +func (p *Markdown) addChild(node NodeType, offset uint32) *Node { + return p.addExistingChild(NewNode(node), offset) +} + +func (p *Markdown) addExistingChild(node *Node, offset uint32) *Node { + for !p.tip.canContain(node.Type) { + p.finalize(p.tip) + } + p.tip.AppendChild(node) + p.tip = node + return node +} + +func (p *Markdown) closeUnmatchedBlocks() { + if !p.allClosed { + for p.oldTip != p.lastMatchedContainer { + parent := p.oldTip.Parent + p.finalize(p.oldTip) + p.oldTip = parent + } + p.allClosed = true + } +} + +// +// +// Public interface +// +// + +// Reference represents the details of a link. +// See the documentation in Options for more details on use-case. +type Reference struct { + // Link is usually the URL the reference points to. + Link string + // Title is the alternate text describing the link in more detail. + Title string + // Text is the optional text to override the ref with if the syntax used was + // [refid][] + Text string +} + +// ReferenceOverrideFunc is expected to be called with a reference string and +// return either a valid Reference type that the reference string maps to or +// nil. If overridden is false, the default reference logic will be executed. +// See the documentation in Options for more details on use-case. +type ReferenceOverrideFunc func(reference string) (ref *Reference, overridden bool) + +// New constructs a Markdown processor. You can use the same With* functions as +// for Run() to customize parser's behavior and the renderer. +func New(opts ...Option) *Markdown { + var p Markdown + for _, opt := range opts { + opt(&p) + } + p.refs = make(map[string]*reference) + p.maxNesting = 16 + p.insideLink = false + docNode := NewNode(Document) + p.doc = docNode + p.tip = docNode + p.oldTip = docNode + p.lastMatchedContainer = docNode + p.allClosed = true + // register inline parsers + p.inlineCallback[' '] = maybeLineBreak + p.inlineCallback['*'] = emphasis + p.inlineCallback['_'] = emphasis + if p.extensions&Strikethrough != 0 { + p.inlineCallback['~'] = emphasis + } + p.inlineCallback['`'] = codeSpan + p.inlineCallback['\n'] = lineBreak + p.inlineCallback['['] = link + p.inlineCallback['<'] = leftAngle + p.inlineCallback['\\'] = escape + p.inlineCallback['&'] = entity + p.inlineCallback['!'] = maybeImage + p.inlineCallback['^'] = maybeInlineFootnote + if p.extensions&Autolink != 0 { + p.inlineCallback['h'] = maybeAutoLink + p.inlineCallback['m'] = maybeAutoLink + p.inlineCallback['f'] = maybeAutoLink + p.inlineCallback['H'] = maybeAutoLink + p.inlineCallback['M'] = maybeAutoLink + p.inlineCallback['F'] = maybeAutoLink + } + if p.extensions&Footnotes != 0 { + p.notes = make([]*reference, 0) + } + return &p +} + +// Option customizes the Markdown processor's default behavior. +type Option func(*Markdown) + +// WithRenderer allows you to override the default renderer. +func WithRenderer(r Renderer) Option { + return func(p *Markdown) { + p.renderer = r + } +} + +// WithExtensions allows you to pick some of the many extensions provided by +// Blackfriday. You can bitwise OR them. +func WithExtensions(e Extensions) Option { + return func(p *Markdown) { + p.extensions = e + } +} + +// WithNoExtensions turns off all extensions and custom behavior. +func WithNoExtensions() Option { + return func(p *Markdown) { + p.extensions = NoExtensions + p.renderer = NewHTMLRenderer(HTMLRendererParameters{ + Flags: HTMLFlagsNone, + }) + } +} + +// WithRefOverride sets an optional function callback that is called every +// time a reference is resolved. +// +// In Markdown, the link reference syntax can be made to resolve a link to +// a reference instead of an inline URL, in one of the following ways: +// +// * [link text][refid] +// * [refid][] +// +// Usually, the refid is defined at the bottom of the Markdown document. If +// this override function is provided, the refid is passed to the override +// function first, before consulting the defined refids at the bottom. If +// the override function indicates an override did not occur, the refids at +// the bottom will be used to fill in the link details. +func WithRefOverride(o ReferenceOverrideFunc) Option { + return func(p *Markdown) { + p.referenceOverride = o + } +} + +// Run is the main entry point to Blackfriday. It parses and renders a +// block of markdown-encoded text. +// +// The simplest invocation of Run takes one argument, input: +// output := Run(input) +// This will parse the input with CommonExtensions enabled and render it with +// the default HTMLRenderer (with CommonHTMLFlags). +// +// Variadic arguments opts can customize the default behavior. Since Markdown +// type does not contain exported fields, you can not use it directly. Instead, +// use the With* functions. For example, this will call the most basic +// functionality, with no extensions: +// output := Run(input, WithNoExtensions()) +// +// You can use any number of With* arguments, even contradicting ones. They +// will be applied in order of appearance and the latter will override the +// former: +// output := Run(input, WithNoExtensions(), WithExtensions(exts), +// WithRenderer(yourRenderer)) +func Run(input []byte, opts ...Option) []byte { + r := NewHTMLRenderer(HTMLRendererParameters{ + Flags: CommonHTMLFlags, + }) + optList := []Option{WithRenderer(r), WithExtensions(CommonExtensions)} + optList = append(optList, opts...) + parser := New(optList...) + ast := parser.Parse(input) + var buf bytes.Buffer + parser.renderer.RenderHeader(&buf, ast) + ast.Walk(func(node *Node, entering bool) WalkStatus { + return parser.renderer.RenderNode(&buf, node, entering) + }) + parser.renderer.RenderFooter(&buf, ast) + return buf.Bytes() +} + +// Parse is an entry point to the parsing part of Blackfriday. It takes an +// input markdown document and produces a syntax tree for its contents. This +// tree can then be rendered with a default or custom renderer, or +// analyzed/transformed by the caller to whatever non-standard needs they have. +// The return value is the root node of the syntax tree. +func (p *Markdown) Parse(input []byte) *Node { + p.block(input) + // Walk the tree and finish up some of unfinished blocks + for p.tip != nil { + p.finalize(p.tip) + } + // Walk the tree again and process inline markdown in each block + p.doc.Walk(func(node *Node, entering bool) WalkStatus { + if node.Type == Paragraph || node.Type == Heading || node.Type == TableCell { + p.inline(node, node.content) + node.content = nil + } + return GoToNext + }) + p.parseRefsToAST() + return p.doc +} + +func (p *Markdown) parseRefsToAST() { + if p.extensions&Footnotes == 0 || len(p.notes) == 0 { + return + } + p.tip = p.doc + block := p.addBlock(List, nil) + block.IsFootnotesList = true + block.ListFlags = ListTypeOrdered + flags := ListItemBeginningOfList + // Note: this loop is intentionally explicit, not range-form. This is + // because the body of the loop will append nested footnotes to p.notes and + // we need to process those late additions. Range form would only walk over + // the fixed initial set. + for i := 0; i < len(p.notes); i++ { + ref := p.notes[i] + p.addExistingChild(ref.footnote, 0) + block := ref.footnote + block.ListFlags = flags | ListTypeOrdered + block.RefLink = ref.link + if ref.hasBlock { + flags |= ListItemContainsBlock + p.block(ref.title) + } else { + p.inline(block, ref.title) + } + flags &^= ListItemBeginningOfList | ListItemContainsBlock + } + above := block.Parent + finalizeList(block) + p.tip = above + block.Walk(func(node *Node, entering bool) WalkStatus { + if node.Type == Paragraph || node.Type == Heading { + p.inline(node, node.content) + node.content = nil + } + return GoToNext + }) +} + +// +// Link references +// +// This section implements support for references that (usually) appear +// as footnotes in a document, and can be referenced anywhere in the document. +// The basic format is: +// +// [1]: http://www.google.com/ "Google" +// [2]: http://www.github.com/ "Github" +// +// Anywhere in the document, the reference can be linked by referring to its +// label, i.e., 1 and 2 in this example, as in: +// +// This library is hosted on [Github][2], a git hosting site. +// +// Actual footnotes as specified in Pandoc and supported by some other Markdown +// libraries such as php-markdown are also taken care of. They look like this: +// +// This sentence needs a bit of further explanation.[^note] +// +// [^note]: This is the explanation. +// +// Footnotes should be placed at the end of the document in an ordered list. +// Inline footnotes such as: +// +// Inline footnotes^[Not supported.] also exist. +// +// are not yet supported. + +// reference holds all information necessary for a reference-style links or +// footnotes. +// +// Consider this markdown with reference-style links: +// +// [link][ref] +// +// [ref]: /url/ "tooltip title" +// +// It will be ultimately converted to this HTML: +// +//

    link

    +// +// And a reference structure will be populated as follows: +// +// p.refs["ref"] = &reference{ +// link: "/url/", +// title: "tooltip title", +// } +// +// Alternatively, reference can contain information about a footnote. Consider +// this markdown: +// +// Text needing a footnote.[^a] +// +// [^a]: This is the note +// +// A reference structure will be populated as follows: +// +// p.refs["a"] = &reference{ +// link: "a", +// title: "This is the note", +// noteID: , +// } +// +// TODO: As you can see, it begs for splitting into two dedicated structures +// for refs and for footnotes. +type reference struct { + link []byte + title []byte + noteID int // 0 if not a footnote ref + hasBlock bool + footnote *Node // a link to the Item node within a list of footnotes + + text []byte // only gets populated by refOverride feature with Reference.Text +} + +func (r *reference) String() string { + return fmt.Sprintf("{link: %q, title: %q, text: %q, noteID: %d, hasBlock: %v}", + r.link, r.title, r.text, r.noteID, r.hasBlock) +} + +// Check whether or not data starts with a reference link. +// If so, it is parsed and stored in the list of references +// (in the render struct). +// Returns the number of bytes to skip to move past it, +// or zero if the first line is not a reference. +func isReference(p *Markdown, data []byte, tabSize int) int { + // up to 3 optional leading spaces + if len(data) < 4 { + return 0 + } + i := 0 + for i < 3 && data[i] == ' ' { + i++ + } + + noteID := 0 + + // id part: anything but a newline between brackets + if data[i] != '[' { + return 0 + } + i++ + if p.extensions&Footnotes != 0 { + if i < len(data) && data[i] == '^' { + // we can set it to anything here because the proper noteIds will + // be assigned later during the second pass. It just has to be != 0 + noteID = 1 + i++ + } + } + idOffset := i + for i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != ']' { + i++ + } + if i >= len(data) || data[i] != ']' { + return 0 + } + idEnd := i + // footnotes can have empty ID, like this: [^], but a reference can not be + // empty like this: []. Break early if it's not a footnote and there's no ID + if noteID == 0 && idOffset == idEnd { + return 0 + } + // spacer: colon (space | tab)* newline? (space | tab)* + i++ + if i >= len(data) || data[i] != ':' { + return 0 + } + i++ + for i < len(data) && (data[i] == ' ' || data[i] == '\t') { + i++ + } + if i < len(data) && (data[i] == '\n' || data[i] == '\r') { + i++ + if i < len(data) && data[i] == '\n' && data[i-1] == '\r' { + i++ + } + } + for i < len(data) && (data[i] == ' ' || data[i] == '\t') { + i++ + } + if i >= len(data) { + return 0 + } + + var ( + linkOffset, linkEnd int + titleOffset, titleEnd int + lineEnd int + raw []byte + hasBlock bool + ) + + if p.extensions&Footnotes != 0 && noteID != 0 { + linkOffset, linkEnd, raw, hasBlock = scanFootnote(p, data, i, tabSize) + lineEnd = linkEnd + } else { + linkOffset, linkEnd, titleOffset, titleEnd, lineEnd = scanLinkRef(p, data, i) + } + if lineEnd == 0 { + return 0 + } + + // a valid ref has been found + + ref := &reference{ + noteID: noteID, + hasBlock: hasBlock, + } + + if noteID > 0 { + // reusing the link field for the id since footnotes don't have links + ref.link = data[idOffset:idEnd] + // if footnote, it's not really a title, it's the contained text + ref.title = raw + } else { + ref.link = data[linkOffset:linkEnd] + ref.title = data[titleOffset:titleEnd] + } + + // id matches are case-insensitive + id := string(bytes.ToLower(data[idOffset:idEnd])) + + p.refs[id] = ref + + return lineEnd +} + +func scanLinkRef(p *Markdown, data []byte, i int) (linkOffset, linkEnd, titleOffset, titleEnd, lineEnd int) { + // link: whitespace-free sequence, optionally between angle brackets + if data[i] == '<' { + i++ + } + linkOffset = i + for i < len(data) && data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' { + i++ + } + linkEnd = i + if data[linkOffset] == '<' && data[linkEnd-1] == '>' { + linkOffset++ + linkEnd-- + } + + // optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) + for i < len(data) && (data[i] == ' ' || data[i] == '\t') { + i++ + } + if i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != '\'' && data[i] != '"' && data[i] != '(' { + return + } + + // compute end-of-line + if i >= len(data) || data[i] == '\r' || data[i] == '\n' { + lineEnd = i + } + if i+1 < len(data) && data[i] == '\r' && data[i+1] == '\n' { + lineEnd++ + } + + // optional (space|tab)* spacer after a newline + if lineEnd > 0 { + i = lineEnd + 1 + for i < len(data) && (data[i] == ' ' || data[i] == '\t') { + i++ + } + } + + // optional title: any non-newline sequence enclosed in '"() alone on its line + if i+1 < len(data) && (data[i] == '\'' || data[i] == '"' || data[i] == '(') { + i++ + titleOffset = i + + // look for EOL + for i < len(data) && data[i] != '\n' && data[i] != '\r' { + i++ + } + if i+1 < len(data) && data[i] == '\n' && data[i+1] == '\r' { + titleEnd = i + 1 + } else { + titleEnd = i + } + + // step back + i-- + for i > titleOffset && (data[i] == ' ' || data[i] == '\t') { + i-- + } + if i > titleOffset && (data[i] == '\'' || data[i] == '"' || data[i] == ')') { + lineEnd = titleEnd + titleEnd = i + } + } + + return +} + +// The first bit of this logic is the same as Parser.listItem, but the rest +// is much simpler. This function simply finds the entire block and shifts it +// over by one tab if it is indeed a block (just returns the line if it's not). +// blockEnd is the end of the section in the input buffer, and contents is the +// extracted text that was shifted over one tab. It will need to be rendered at +// the end of the document. +func scanFootnote(p *Markdown, data []byte, i, indentSize int) (blockStart, blockEnd int, contents []byte, hasBlock bool) { + if i == 0 || len(data) == 0 { + return + } + + // skip leading whitespace on first line + for i < len(data) && data[i] == ' ' { + i++ + } + + blockStart = i + + // find the end of the line + blockEnd = i + for i < len(data) && data[i-1] != '\n' { + i++ + } + + // get working buffer + var raw bytes.Buffer + + // put the first line into the working buffer + raw.Write(data[blockEnd:i]) + blockEnd = i + + // process the following lines + containsBlankLine := false + +gatherLines: + for blockEnd < len(data) { + i++ + + // find the end of this line + for i < len(data) && data[i-1] != '\n' { + i++ + } + + // if it is an empty line, guess that it is part of this item + // and move on to the next line + if p.isEmpty(data[blockEnd:i]) > 0 { + containsBlankLine = true + blockEnd = i + continue + } + + n := 0 + if n = isIndented(data[blockEnd:i], indentSize); n == 0 { + // this is the end of the block. + // we don't want to include this last line in the index. + break gatherLines + } + + // if there were blank lines before this one, insert a new one now + if containsBlankLine { + raw.WriteByte('\n') + containsBlankLine = false + } + + // get rid of that first tab, write to buffer + raw.Write(data[blockEnd+n : i]) + hasBlock = true + + blockEnd = i + } + + if data[blockEnd-1] != '\n' { + raw.WriteByte('\n') + } + + contents = raw.Bytes() + + return +} + +// +// +// Miscellaneous helper functions +// +// + +// Test if a character is a punctuation symbol. +// Taken from a private function in regexp in the stdlib. +func ispunct(c byte) bool { + for _, r := range []byte("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") { + if c == r { + return true + } + } + return false +} + +// Test if a character is a whitespace character. +func isspace(c byte) bool { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v' +} + +// Test if a character is letter. +func isletter(c byte) bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') +} + +// Test if a character is a letter or a digit. +// TODO: check when this is looking for ASCII alnum and when it should use unicode +func isalnum(c byte) bool { + return (c >= '0' && c <= '9') || isletter(c) +} + +// Replace tab characters with spaces, aligning to the next TAB_SIZE column. +// always ends output with a newline +func expandTabs(out *bytes.Buffer, line []byte, tabSize int) { + // first, check for common cases: no tabs, or only tabs at beginning of line + i, prefix := 0, 0 + slowcase := false + for i = 0; i < len(line); i++ { + if line[i] == '\t' { + if prefix == i { + prefix++ + } else { + slowcase = true + break + } + } + } + + // no need to decode runes if all tabs are at the beginning of the line + if !slowcase { + for i = 0; i < prefix*tabSize; i++ { + out.WriteByte(' ') + } + out.Write(line[prefix:]) + return + } + + // the slow case: we need to count runes to figure out how + // many spaces to insert for each tab + column := 0 + i = 0 + for i < len(line) { + start := i + for i < len(line) && line[i] != '\t' { + _, size := utf8.DecodeRune(line[i:]) + i += size + column++ + } + + if i > start { + out.Write(line[start:i]) + } + + if i >= len(line) { + break + } + + for { + out.WriteByte(' ') + column++ + if column%tabSize == 0 { + break + } + } + + i++ + } +} + +// Find if a line counts as indented or not. +// Returns number of characters the indent is (0 = not indented). +func isIndented(data []byte, indentSize int) int { + if len(data) == 0 { + return 0 + } + if data[0] == '\t' { + return 1 + } + if len(data) < indentSize { + return 0 + } + for i := 0; i < indentSize; i++ { + if data[i] != ' ' { + return 0 + } + } + return indentSize +} + +// Create a url-safe slug for fragments +func slugify(in []byte) []byte { + if len(in) == 0 { + return in + } + out := make([]byte, 0, len(in)) + sym := false + + for _, ch := range in { + if isalnum(ch) { + sym = false + out = append(out, ch) + } else if sym { + continue + } else { + out = append(out, '-') + sym = true + } + } + var a, b int + var ch byte + for a, ch = range out { + if ch != '-' { + break + } + } + for b = len(out) - 1; b > 0; b-- { + if out[b] != '-' { + break + } + } + return out[a : b+1] +} diff --git a/pkg/blackfriday/markdown_test.go b/pkg/blackfriday/markdown_test.go new file mode 100644 index 00000000..2c7a262b --- /dev/null +++ b/pkg/blackfriday/markdown_test.go @@ -0,0 +1,38 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// Unit tests for full document parsing and rendering +// + +package blackfriday + +import "testing" + +func TestDocument(t *testing.T) { + var tests = []string{ + // Empty document. + "", + "", + + " ", + "", + + // This shouldn't panic. + // https://github.com/russross/blackfriday/issues/172 + "[]:<", + "

    []:<

    \n", + + // This shouldn't panic. + // https://github.com/russross/blackfriday/issues/173 + " [", + "

    [

    \n", + } + doTests(t, tests) +} diff --git a/pkg/blackfriday/node.go b/pkg/blackfriday/node.go new file mode 100644 index 00000000..51b9e8c1 --- /dev/null +++ b/pkg/blackfriday/node.go @@ -0,0 +1,354 @@ +package blackfriday + +import ( + "bytes" + "fmt" +) + +// NodeType specifies a type of a single node of a syntax tree. Usually one +// node (and its type) corresponds to a single markdown feature, e.g. emphasis +// or code block. +type NodeType int + +// Constants for identifying different types of nodes. See NodeType. +const ( + Document NodeType = iota + BlockQuote + List + Item + Paragraph + Heading + HorizontalRule + Emph + Strong + Del + Link + Image + Text + HTMLBlock + CodeBlock + Softbreak + Hardbreak + Code + HTMLSpan + Table + TableCell + TableHead + TableBody + TableRow +) + +var nodeTypeNames = []string{ + Document: "Document", + BlockQuote: "BlockQuote", + List: "List", + Item: "Item", + Paragraph: "Paragraph", + Heading: "Heading", + HorizontalRule: "HorizontalRule", + Emph: "Emph", + Strong: "Strong", + Del: "Del", + Link: "Link", + Image: "Image", + Text: "Text", + HTMLBlock: "HTMLBlock", + CodeBlock: "CodeBlock", + Softbreak: "Softbreak", + Hardbreak: "Hardbreak", + Code: "Code", + HTMLSpan: "HTMLSpan", + Table: "Table", + TableCell: "TableCell", + TableHead: "TableHead", + TableBody: "TableBody", + TableRow: "TableRow", +} + +func (t NodeType) String() string { + return nodeTypeNames[t] +} + +// ListData contains fields relevant to a List and Item node type. +type ListData struct { + ListFlags ListType + Tight bool // Skip

    s around list item data if true + BulletChar byte // '*', '+' or '-' in bullet lists + Delimiter byte // '.' or ')' after the number in ordered lists + RefLink []byte // If not nil, turns this list item into a footnote item and triggers different rendering + IsFootnotesList bool // This is a list of footnotes +} + +// LinkData contains fields relevant to a Link node type. +type LinkData struct { + Destination []byte // Destination is what goes into a href + Title []byte // Title is the tooltip thing that goes in a title attribute + NoteID int // NoteID contains a serial number of a footnote, zero if it's not a footnote + Footnote *Node // If it's a footnote, this is a direct link to the footnote Node. Otherwise nil. +} + +// CodeBlockData contains fields relevant to a CodeBlock node type. +type CodeBlockData struct { + IsFenced bool // Specifies whether it's a fenced code block or an indented one + Info []byte // This holds the info string + FenceChar byte + FenceLength int + FenceOffset int +} + +// TableCellData contains fields relevant to a TableCell node type. +type TableCellData struct { + IsHeader bool // This tells if it's under the header row + Align CellAlignFlags // This holds the value for align attribute +} + +// HeadingData contains fields relevant to a Heading node type. +type HeadingData struct { + Level int // This holds the heading level number + HeadingID string // This might hold heading ID, if present + IsTitleblock bool // Specifies whether it's a title block +} + +// Node is a single element in the abstract syntax tree of the parsed document. +// It holds connections to the structurally neighboring nodes and, for certain +// types of nodes, additional information that might be needed when rendering. +type Node struct { + Type NodeType // Determines the type of the node + Parent *Node // Points to the parent + FirstChild *Node // Points to the first child, if any + LastChild *Node // Points to the last child, if any + Prev *Node // Previous sibling; nil if it's the first child + Next *Node // Next sibling; nil if it's the last child + + Literal []byte // Text contents of the leaf nodes + + HeadingData // Populated if Type is Heading + ListData // Populated if Type is List + CodeBlockData // Populated if Type is CodeBlock + LinkData // Populated if Type is Link + TableCellData // Populated if Type is TableCell + + content []byte // Markdown content of the block nodes + open bool // Specifies an open block node that has not been finished to process yet +} + +// NewNode allocates a node of a specified type. +func NewNode(typ NodeType) *Node { + return &Node{ + Type: typ, + open: true, + } +} + +func (n *Node) String() string { + ellipsis := "" + snippet := n.Literal + if len(snippet) > 16 { + snippet = snippet[:16] + ellipsis = "..." + } + return fmt.Sprintf("%s: '%s%s'", n.Type, snippet, ellipsis) +} + +// Unlink removes node 'n' from the tree. +// It panics if the node is nil. +func (n *Node) Unlink() { + if n.Prev != nil { + n.Prev.Next = n.Next + } else if n.Parent != nil { + n.Parent.FirstChild = n.Next + } + if n.Next != nil { + n.Next.Prev = n.Prev + } else if n.Parent != nil { + n.Parent.LastChild = n.Prev + } + n.Parent = nil + n.Next = nil + n.Prev = nil +} + +// AppendChild adds a node 'child' as a child of 'n'. +// It panics if either node is nil. +func (n *Node) AppendChild(child *Node) { + child.Unlink() + child.Parent = n + if n.LastChild != nil { + n.LastChild.Next = child + child.Prev = n.LastChild + n.LastChild = child + } else { + n.FirstChild = child + n.LastChild = child + } +} + +// InsertBefore inserts 'sibling' immediately before 'n'. +// It panics if either node is nil. +func (n *Node) InsertBefore(sibling *Node) { + sibling.Unlink() + sibling.Prev = n.Prev + if sibling.Prev != nil { + sibling.Prev.Next = sibling + } + sibling.Next = n + n.Prev = sibling + sibling.Parent = n.Parent + if sibling.Prev == nil { + sibling.Parent.FirstChild = sibling + } +} + +func (n *Node) isContainer() bool { + switch n.Type { + case Document: + fallthrough + case BlockQuote: + fallthrough + case List: + fallthrough + case Item: + fallthrough + case Paragraph: + fallthrough + case Heading: + fallthrough + case Emph: + fallthrough + case Strong: + fallthrough + case Del: + fallthrough + case Link: + fallthrough + case Image: + fallthrough + case Table: + fallthrough + case TableHead: + fallthrough + case TableBody: + fallthrough + case TableRow: + fallthrough + case TableCell: + return true + default: + return false + } +} + +func (n *Node) canContain(t NodeType) bool { + if n.Type == List { + return t == Item + } + if n.Type == Document || n.Type == BlockQuote || n.Type == Item { + return t != Item + } + if n.Type == Table { + return t == TableHead || t == TableBody + } + if n.Type == TableHead || n.Type == TableBody { + return t == TableRow + } + if n.Type == TableRow { + return t == TableCell + } + return false +} + +// WalkStatus allows NodeVisitor to have some control over the tree traversal. +// It is returned from NodeVisitor and different values allow Node.Walk to +// decide which node to go to next. +type WalkStatus int + +const ( + // GoToNext is the default traversal of every node. + GoToNext WalkStatus = iota + // SkipChildren tells walker to skip all children of current node. + SkipChildren + // Terminate tells walker to terminate the traversal. + Terminate +) + +// NodeVisitor is a callback to be called when traversing the syntax tree. +// Called twice for every node: once with entering=true when the branch is +// first visited, then with entering=false after all the children are done. +type NodeVisitor func(node *Node, entering bool) WalkStatus + +// Walk is a convenience method that instantiates a walker and starts a +// traversal of subtree rooted at n. +func (n *Node) Walk(visitor NodeVisitor) { + w := newNodeWalker(n) + for w.current != nil { + status := visitor(w.current, w.entering) + switch status { + case GoToNext: + w.next() + case SkipChildren: + w.entering = false + w.next() + case Terminate: + return + } + } +} + +type nodeWalker struct { + current *Node + root *Node + entering bool +} + +func newNodeWalker(root *Node) *nodeWalker { + return &nodeWalker{ + current: root, + root: root, + entering: true, + } +} + +func (nw *nodeWalker) next() { + if (!nw.current.isContainer() || !nw.entering) && nw.current == nw.root { + nw.current = nil + return + } + if nw.entering && nw.current.isContainer() { + if nw.current.FirstChild != nil { + nw.current = nw.current.FirstChild + nw.entering = true + } else { + nw.entering = false + } + } else if nw.current.Next == nil { + nw.current = nw.current.Parent + nw.entering = false + } else { + nw.current = nw.current.Next + nw.entering = true + } +} + +func dump(ast *Node) { + fmt.Println(dumpString(ast)) +} + +func dumpR(ast *Node, depth int) string { + if ast == nil { + return "" + } + indent := bytes.Repeat([]byte("\t"), depth) + content := ast.Literal + if content == nil { + content = ast.content + } + result := fmt.Sprintf("%s%s(%q)\n", indent, ast.Type, content) + for n := ast.FirstChild; n != nil; n = n.Next { + result += dumpR(n, depth+1) + } + return result +} + +func dumpString(ast *Node) string { + return dumpR(ast, 0) +} diff --git a/pkg/blackfriday/ref_test.go b/pkg/blackfriday/ref_test.go new file mode 100644 index 00000000..4375f540 --- /dev/null +++ b/pkg/blackfriday/ref_test.go @@ -0,0 +1,124 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// Markdown 1.0.3 reference tests +// + +package blackfriday + +import ( + "io/ioutil" + "path/filepath" + "testing" +) + +func TestReference(t *testing.T) { + files := []string{ + "Amps and angle encoding", + "Auto links", + "Backslash escapes", + "Blockquotes with code blocks", + "Code Blocks", + "Code Spans", + "Hard-wrapped paragraphs with list-like lines", + "Horizontal rules", + "Inline HTML (Advanced)", + "Inline HTML (Simple)", + "Inline HTML comments", + "Links, inline style", + "Links, reference style", + "Links, shortcut references", + "Literal quotes in titles", + "Markdown Documentation - Basics", + "Markdown Documentation - Syntax", + "Nested blockquotes", + "Ordered and unordered lists", + "Strong and em together", + "Tabs", + "Tidyness", + } + doTestsReference(t, files, 0) +} + +func TestReference_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK(t *testing.T) { + files := []string{ + "Amps and angle encoding", + "Auto links", + "Backslash escapes", + "Blockquotes with code blocks", + "Code Blocks", + "Code Spans", + "Hard-wrapped paragraphs with list-like lines no empty line before block", + "Horizontal rules", + "Inline HTML (Advanced)", + "Inline HTML (Simple)", + "Inline HTML comments", + "Links, inline style", + "Links, reference style", + "Links, shortcut references", + "Literal quotes in titles", + "Markdown Documentation - Basics", + "Markdown Documentation - Syntax", + "Nested blockquotes", + "Ordered and unordered lists", + "Strong and em together", + "Tabs", + "Tidyness", + } + doTestsReference(t, files, NoEmptyLineBeforeBlock) +} + +// benchResultAnchor is an anchor variable to store the result of a benchmarked +// code so that compiler could never optimize away the call to runMarkdown() +var benchResultAnchor string + +func BenchmarkReference(b *testing.B) { + params := TestParams{extensions: CommonExtensions} + files := []string{ + "Amps and angle encoding", + "Auto links", + "Backslash escapes", + "Blockquotes with code blocks", + "Code Blocks", + "Code Spans", + "Hard-wrapped paragraphs with list-like lines", + "Horizontal rules", + "Inline HTML (Advanced)", + "Inline HTML (Simple)", + "Inline HTML comments", + "Links, inline style", + "Links, reference style", + "Links, shortcut references", + "Literal quotes in titles", + "Markdown Documentation - Basics", + "Markdown Documentation - Syntax", + "Nested blockquotes", + "Ordered and unordered lists", + "Strong and em together", + "Tabs", + "Tidyness", + } + var tests []string + for _, basename := range files { + filename := filepath.Join("testdata", basename+".text") + inputBytes, err := ioutil.ReadFile(filename) + if err != nil { + b.Errorf("Couldn't open '%s', error: %v\n", filename, err) + continue + } + tests = append(tests, string(inputBytes)) + } + b.ResetTimer() + for n := 0; n < b.N; n++ { + for _, test := range tests { + benchResultAnchor = runMarkdown(test, params) + } + } +} diff --git a/pkg/blackfriday/smartypants.go b/pkg/blackfriday/smartypants.go new file mode 100644 index 00000000..3a220e94 --- /dev/null +++ b/pkg/blackfriday/smartypants.go @@ -0,0 +1,457 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// +// SmartyPants rendering +// +// + +package blackfriday + +import ( + "bytes" + "io" +) + +// SPRenderer is a struct containing state of a Smartypants renderer. +type SPRenderer struct { + inSingleQuote bool + inDoubleQuote bool + callbacks [256]smartCallback +} + +func wordBoundary(c byte) bool { + return c == 0 || isspace(c) || ispunct(c) +} + +func tolower(c byte) byte { + if c >= 'A' && c <= 'Z' { + return c - 'A' + 'a' + } + return c +} + +func isdigit(c byte) bool { + return c >= '0' && c <= '9' +} + +func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool, addNBSP bool) bool { + // edge of the buffer is likely to be a tag that we don't get to see, + // so we treat it like text sometimes + + // enumerate all sixteen possibilities for (previousChar, nextChar) + // each can be one of {0, space, punct, other} + switch { + case previousChar == 0 && nextChar == 0: + // context is not any help here, so toggle + *isOpen = !*isOpen + case isspace(previousChar) && nextChar == 0: + // [ "] might be [ "foo...] + *isOpen = true + case ispunct(previousChar) && nextChar == 0: + // [!"] hmm... could be [Run!"] or [("...] + *isOpen = false + case /* isnormal(previousChar) && */ nextChar == 0: + // [a"] is probably a close + *isOpen = false + case previousChar == 0 && isspace(nextChar): + // [" ] might be [...foo" ] + *isOpen = false + case isspace(previousChar) && isspace(nextChar): + // [ " ] context is not any help here, so toggle + *isOpen = !*isOpen + case ispunct(previousChar) && isspace(nextChar): + // [!" ] is probably a close + *isOpen = false + case /* isnormal(previousChar) && */ isspace(nextChar): + // [a" ] this is one of the easy cases + *isOpen = false + case previousChar == 0 && ispunct(nextChar): + // ["!] hmm... could be ["$1.95] or ["!...] + *isOpen = false + case isspace(previousChar) && ispunct(nextChar): + // [ "!] looks more like [ "$1.95] + *isOpen = true + case ispunct(previousChar) && ispunct(nextChar): + // [!"!] context is not any help here, so toggle + *isOpen = !*isOpen + case /* isnormal(previousChar) && */ ispunct(nextChar): + // [a"!] is probably a close + *isOpen = false + case previousChar == 0 /* && isnormal(nextChar) */ : + // ["a] is probably an open + *isOpen = true + case isspace(previousChar) /* && isnormal(nextChar) */ : + // [ "a] this is one of the easy cases + *isOpen = true + case ispunct(previousChar) /* && isnormal(nextChar) */ : + // [!"a] is probably an open + *isOpen = true + default: + // [a'b] maybe a contraction? + *isOpen = false + } + + // Note that with the limited lookahead, this non-breaking + // space will also be appended to single double quotes. + if addNBSP && !*isOpen { + out.WriteString(" ") + } + + out.WriteByte('&') + if *isOpen { + out.WriteByte('l') + } else { + out.WriteByte('r') + } + out.WriteByte(quote) + out.WriteString("quo;") + + if addNBSP && *isOpen { + out.WriteString(" ") + } + + return true +} + +func (r *SPRenderer) smartSingleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 2 { + t1 := tolower(text[1]) + + if t1 == '\'' { + nextChar := byte(0) + if len(text) >= 3 { + nextChar = text[2] + } + if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) { + return 1 + } + } + + if (t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && (len(text) < 3 || wordBoundary(text[2])) { + out.WriteString("’") + return 0 + } + + if len(text) >= 3 { + t2 := tolower(text[2]) + + if ((t1 == 'r' && t2 == 'e') || (t1 == 'l' && t2 == 'l') || (t1 == 'v' && t2 == 'e')) && + (len(text) < 4 || wordBoundary(text[3])) { + out.WriteString("’") + return 0 + } + } + } + + nextChar := byte(0) + if len(text) > 1 { + nextChar = text[1] + } + if smartQuoteHelper(out, previousChar, nextChar, 's', &r.inSingleQuote, false) { + return 0 + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartParens(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 3 { + t1 := tolower(text[1]) + t2 := tolower(text[2]) + + if t1 == 'c' && t2 == ')' { + out.WriteString("©") + return 2 + } + + if t1 == 'r' && t2 == ')' { + out.WriteString("®") + return 2 + } + + if len(text) >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')' { + out.WriteString("™") + return 3 + } + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartDash(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 2 { + if text[1] == '-' { + out.WriteString("—") + return 1 + } + + if wordBoundary(previousChar) && wordBoundary(text[1]) { + out.WriteString("–") + return 0 + } + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartDashLatex(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 3 && text[1] == '-' && text[2] == '-' { + out.WriteString("—") + return 2 + } + if len(text) >= 2 && text[1] == '-' { + out.WriteString("–") + return 1 + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartAmpVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte, addNBSP bool) int { + if bytes.HasPrefix(text, []byte(""")) { + nextChar := byte(0) + if len(text) >= 7 { + nextChar = text[6] + } + if smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, addNBSP) { + return 5 + } + } + + if bytes.HasPrefix(text, []byte("�")) { + return 3 + } + + out.WriteByte('&') + return 0 +} + +func (r *SPRenderer) smartAmp(angledQuotes, addNBSP bool) func(*bytes.Buffer, byte, []byte) int { + var quote byte = 'd' + if angledQuotes { + quote = 'a' + } + + return func(out *bytes.Buffer, previousChar byte, text []byte) int { + return r.smartAmpVariant(out, previousChar, text, quote, addNBSP) + } +} + +func (r *SPRenderer) smartPeriod(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 3 && text[1] == '.' && text[2] == '.' { + out.WriteString("…") + return 2 + } + + if len(text) >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.' { + out.WriteString("…") + return 4 + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartBacktick(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 2 && text[1] == '`' { + nextChar := byte(0) + if len(text) >= 3 { + nextChar = text[2] + } + if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) { + return 1 + } + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartNumberGeneric(out *bytes.Buffer, previousChar byte, text []byte) int { + if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 { + // is it of the form digits/digits(word boundary)?, i.e., \d+/\d+\b + // note: check for regular slash (/) or fraction slash (⁄, 0x2044, or 0xe2 81 84 in utf-8) + // and avoid changing dates like 1/23/2005 into fractions. + numEnd := 0 + for len(text) > numEnd && isdigit(text[numEnd]) { + numEnd++ + } + if numEnd == 0 { + out.WriteByte(text[0]) + return 0 + } + denStart := numEnd + 1 + if len(text) > numEnd+3 && text[numEnd] == 0xe2 && text[numEnd+1] == 0x81 && text[numEnd+2] == 0x84 { + denStart = numEnd + 3 + } else if len(text) < numEnd+2 || text[numEnd] != '/' { + out.WriteByte(text[0]) + return 0 + } + denEnd := denStart + for len(text) > denEnd && isdigit(text[denEnd]) { + denEnd++ + } + if denEnd == denStart { + out.WriteByte(text[0]) + return 0 + } + if len(text) == denEnd || wordBoundary(text[denEnd]) && text[denEnd] != '/' { + out.WriteString("") + out.Write(text[:numEnd]) + out.WriteString("") + out.Write(text[denStart:denEnd]) + out.WriteString("") + return denEnd - 1 + } + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartNumber(out *bytes.Buffer, previousChar byte, text []byte) int { + if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 { + if text[0] == '1' && text[1] == '/' && text[2] == '2' { + if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' { + out.WriteString("½") + return 2 + } + } + + if text[0] == '1' && text[1] == '/' && text[2] == '4' { + if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h') { + out.WriteString("¼") + return 2 + } + } + + if text[0] == '3' && text[1] == '/' && text[2] == '4' { + if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's') { + out.WriteString("¾") + return 2 + } + } + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartDoubleQuoteVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte) int { + nextChar := byte(0) + if len(text) > 1 { + nextChar = text[1] + } + if !smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, false) { + out.WriteString(""") + } + + return 0 +} + +func (r *SPRenderer) smartDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { + return r.smartDoubleQuoteVariant(out, previousChar, text, 'd') +} + +func (r *SPRenderer) smartAngledDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { + return r.smartDoubleQuoteVariant(out, previousChar, text, 'a') +} + +func (r *SPRenderer) smartLeftAngle(out *bytes.Buffer, previousChar byte, text []byte) int { + i := 0 + + for i < len(text) && text[i] != '>' { + i++ + } + + out.Write(text[:i+1]) + return i +} + +type smartCallback func(out *bytes.Buffer, previousChar byte, text []byte) int + +// NewSmartypantsRenderer constructs a Smartypants renderer object. +func NewSmartypantsRenderer(flags HTMLFlags) *SPRenderer { + var ( + r SPRenderer + + smartAmpAngled = r.smartAmp(true, false) + smartAmpAngledNBSP = r.smartAmp(true, true) + smartAmpRegular = r.smartAmp(false, false) + smartAmpRegularNBSP = r.smartAmp(false, true) + + addNBSP = flags&SmartypantsQuotesNBSP != 0 + ) + + if flags&SmartypantsAngledQuotes == 0 { + r.callbacks['"'] = r.smartDoubleQuote + if !addNBSP { + r.callbacks['&'] = smartAmpRegular + } else { + r.callbacks['&'] = smartAmpRegularNBSP + } + } else { + r.callbacks['"'] = r.smartAngledDoubleQuote + if !addNBSP { + r.callbacks['&'] = smartAmpAngled + } else { + r.callbacks['&'] = smartAmpAngledNBSP + } + } + r.callbacks['\''] = r.smartSingleQuote + r.callbacks['('] = r.smartParens + if flags&SmartypantsDashes != 0 { + if flags&SmartypantsLatexDashes == 0 { + r.callbacks['-'] = r.smartDash + } else { + r.callbacks['-'] = r.smartDashLatex + } + } + r.callbacks['.'] = r.smartPeriod + if flags&SmartypantsFractions == 0 { + r.callbacks['1'] = r.smartNumber + r.callbacks['3'] = r.smartNumber + } else { + for ch := '1'; ch <= '9'; ch++ { + r.callbacks[ch] = r.smartNumberGeneric + } + } + r.callbacks['<'] = r.smartLeftAngle + r.callbacks['`'] = r.smartBacktick + return &r +} + +// Process is the entry point of the Smartypants renderer. +func (r *SPRenderer) Process(w io.Writer, text []byte) { + mark := 0 + for i := 0; i < len(text); i++ { + if action := r.callbacks[text[i]]; action != nil { + if i > mark { + w.Write(text[mark:i]) + } + previousChar := byte(0) + if i > 0 { + previousChar = text[i-1] + } + var tmp bytes.Buffer + i += action(&tmp, previousChar, text[i:]) + w.Write(tmp.Bytes()) + mark = i + 1 + } + } + if mark < len(text) { + w.Write(text[mark:]) + } +} diff --git a/pkg/blackfriday/testdata/Amps and angle encoding.html b/pkg/blackfriday/testdata/Amps and angle encoding.html new file mode 100644 index 00000000..483f8ffa --- /dev/null +++ b/pkg/blackfriday/testdata/Amps and angle encoding.html @@ -0,0 +1,17 @@ +

    AT&T has an ampersand in their name.

    + +

    AT&T is another way to write it.

    + +

    This & that.

    + +

    4 < 5.

    + +

    6 > 5.

    + +

    Here's a link with an ampersand in the URL.

    + +

    Here's a link with an amersand in the link text: AT&T.

    + +

    Here's an inline link.

    + +

    Here's an inline link.

    diff --git a/pkg/blackfriday/testdata/Amps and angle encoding.text b/pkg/blackfriday/testdata/Amps and angle encoding.text new file mode 100644 index 00000000..0e9527f9 --- /dev/null +++ b/pkg/blackfriday/testdata/Amps and angle encoding.text @@ -0,0 +1,21 @@ +AT&T has an ampersand in their name. + +AT&T is another way to write it. + +This & that. + +4 < 5. + +6 > 5. + +Here's a [link] [1] with an ampersand in the URL. + +Here's a link with an amersand in the link text: [AT&T] [2]. + +Here's an inline [link](/script?foo=1&bar=2). + +Here's an inline [link](). + + +[1]: http://example.com/?foo=1&bar=2 +[2]: http://att.com/ "AT&T" \ No newline at end of file diff --git a/pkg/blackfriday/testdata/Auto links.html b/pkg/blackfriday/testdata/Auto links.html new file mode 100644 index 00000000..b1791e7f --- /dev/null +++ b/pkg/blackfriday/testdata/Auto links.html @@ -0,0 +1,18 @@ +

    Link: http://example.com/.

    + +

    With an ampersand: http://example.com/?foo=1&bar=2

    + + + +
    +

    Blockquoted: http://example.com/

    +
    + +

    Auto-links should not occur here: <http://example.com/>

    + +
    or here: <http://example.com/>
    +
    diff --git a/pkg/blackfriday/testdata/Auto links.text b/pkg/blackfriday/testdata/Auto links.text new file mode 100644 index 00000000..abbc4886 --- /dev/null +++ b/pkg/blackfriday/testdata/Auto links.text @@ -0,0 +1,13 @@ +Link: . + +With an ampersand: + +* In a list? +* +* It should. + +> Blockquoted: + +Auto-links should not occur here: `` + + or here: \ No newline at end of file diff --git a/pkg/blackfriday/testdata/Backslash escapes.html b/pkg/blackfriday/testdata/Backslash escapes.html new file mode 100644 index 00000000..a73c998f --- /dev/null +++ b/pkg/blackfriday/testdata/Backslash escapes.html @@ -0,0 +1,123 @@ +

    These should all get escaped:

    + +

    Backslash: \

    + +

    Backtick: `

    + +

    Asterisk: *

    + +

    Underscore: _

    + +

    Left brace: {

    + +

    Right brace: }

    + +

    Left bracket: [

    + +

    Right bracket: ]

    + +

    Left paren: (

    + +

    Right paren: )

    + +

    Greater-than: >

    + +

    Hash: #

    + +

    Period: .

    + +

    Bang: !

    + +

    Plus: +

    + +

    Minus: -

    + +

    Tilde: ~

    + +

    These should not, because they occur within a code block:

    + +
    Backslash: \\
    +
    +Backtick: \`
    +
    +Asterisk: \*
    +
    +Underscore: \_
    +
    +Left brace: \{
    +
    +Right brace: \}
    +
    +Left bracket: \[
    +
    +Right bracket: \]
    +
    +Left paren: \(
    +
    +Right paren: \)
    +
    +Greater-than: \>
    +
    +Hash: \#
    +
    +Period: \.
    +
    +Bang: \!
    +
    +Plus: \+
    +
    +Minus: \-
    +
    +Tilde: \~
    +
    + +

    Nor should these, which occur in code spans:

    + +

    Backslash: \\

    + +

    Backtick: \`

    + +

    Asterisk: \*

    + +

    Underscore: \_

    + +

    Left brace: \{

    + +

    Right brace: \}

    + +

    Left bracket: \[

    + +

    Right bracket: \]

    + +

    Left paren: \(

    + +

    Right paren: \)

    + +

    Greater-than: \>

    + +

    Hash: \#

    + +

    Period: \.

    + +

    Bang: \!

    + +

    Plus: \+

    + +

    Minus: \-

    + +

    Tilde: \~

    + +

    These should get escaped, even though they're matching pairs for +other Markdown constructs:

    + +

    *asterisks*

    + +

    _underscores_

    + +

    `backticks`

    + +

    This is a code span with a literal backslash-backtick sequence: \`

    + +

    This is a tag with unescaped backticks bar.

    + +

    This is a tag with backslashes bar.

    diff --git a/pkg/blackfriday/testdata/Backslash escapes.text b/pkg/blackfriday/testdata/Backslash escapes.text new file mode 100644 index 00000000..04c20bd3 --- /dev/null +++ b/pkg/blackfriday/testdata/Backslash escapes.text @@ -0,0 +1,126 @@ +These should all get escaped: + +Backslash: \\ + +Backtick: \` + +Asterisk: \* + +Underscore: \_ + +Left brace: \{ + +Right brace: \} + +Left bracket: \[ + +Right bracket: \] + +Left paren: \( + +Right paren: \) + +Greater-than: \> + +Hash: \# + +Period: \. + +Bang: \! + +Plus: \+ + +Minus: \- + +Tilde: \~ + + + +These should not, because they occur within a code block: + + Backslash: \\ + + Backtick: \` + + Asterisk: \* + + Underscore: \_ + + Left brace: \{ + + Right brace: \} + + Left bracket: \[ + + Right bracket: \] + + Left paren: \( + + Right paren: \) + + Greater-than: \> + + Hash: \# + + Period: \. + + Bang: \! + + Plus: \+ + + Minus: \- + + Tilde: \~ + + +Nor should these, which occur in code spans: + +Backslash: `\\` + +Backtick: `` \` `` + +Asterisk: `\*` + +Underscore: `\_` + +Left brace: `\{` + +Right brace: `\}` + +Left bracket: `\[` + +Right bracket: `\]` + +Left paren: `\(` + +Right paren: `\)` + +Greater-than: `\>` + +Hash: `\#` + +Period: `\.` + +Bang: `\!` + +Plus: `\+` + +Minus: `\-` + +Tilde: `\~` + + +These should get escaped, even though they're matching pairs for +other Markdown constructs: + +\*asterisks\* + +\_underscores\_ + +\`backticks\` + +This is a code span with a literal backslash-backtick sequence: `` \` `` + +This is a tag with unescaped backticks bar. + +This is a tag with backslashes bar. diff --git a/pkg/blackfriday/testdata/Blockquotes with code blocks.html b/pkg/blackfriday/testdata/Blockquotes with code blocks.html new file mode 100644 index 00000000..360fa9b1 --- /dev/null +++ b/pkg/blackfriday/testdata/Blockquotes with code blocks.html @@ -0,0 +1,15 @@ +
    +

    Example:

    + +
    sub status {
    +    print "working";
    +}
    +
    + +

    Or:

    + +
    sub status {
    +    return "working";
    +}
    +
    +
    diff --git a/pkg/blackfriday/testdata/Blockquotes with code blocks.text b/pkg/blackfriday/testdata/Blockquotes with code blocks.text new file mode 100644 index 00000000..c31d1710 --- /dev/null +++ b/pkg/blackfriday/testdata/Blockquotes with code blocks.text @@ -0,0 +1,11 @@ +> Example: +> +> sub status { +> print "working"; +> } +> +> Or: +> +> sub status { +> return "working"; +> } diff --git a/pkg/blackfriday/testdata/Code Blocks.html b/pkg/blackfriday/testdata/Code Blocks.html new file mode 100644 index 00000000..32703f5c --- /dev/null +++ b/pkg/blackfriday/testdata/Code Blocks.html @@ -0,0 +1,18 @@ +
    code block on the first line
    +
    + +

    Regular text.

    + +
    code block indented by spaces
    +
    + +

    Regular text.

    + +
    the lines in this block  
    +all contain trailing spaces  
    +
    + +

    Regular Text.

    + +
    code block on the last line
    +
    diff --git a/pkg/blackfriday/testdata/Code Blocks.text b/pkg/blackfriday/testdata/Code Blocks.text new file mode 100644 index 00000000..b54b0928 --- /dev/null +++ b/pkg/blackfriday/testdata/Code Blocks.text @@ -0,0 +1,14 @@ + code block on the first line + +Regular text. + + code block indented by spaces + +Regular text. + + the lines in this block + all contain trailing spaces + +Regular Text. + + code block on the last line \ No newline at end of file diff --git a/pkg/blackfriday/testdata/Code Spans.html b/pkg/blackfriday/testdata/Code Spans.html new file mode 100644 index 00000000..ef85f95e --- /dev/null +++ b/pkg/blackfriday/testdata/Code Spans.html @@ -0,0 +1,5 @@ +

    <test a=" content of attribute ">

    + +

    Fix for backticks within HTML tag: like this

    + +

    Here's how you put `backticks` in a code span.

    diff --git a/pkg/blackfriday/testdata/Code Spans.text b/pkg/blackfriday/testdata/Code Spans.text new file mode 100644 index 00000000..750a1973 --- /dev/null +++ b/pkg/blackfriday/testdata/Code Spans.text @@ -0,0 +1,6 @@ +`` + +Fix for backticks within HTML tag: like this + +Here's how you put `` `backticks` `` in a code span. + diff --git a/pkg/blackfriday/testdata/Hard-wrapped paragraphs with list-like lines no empty line before block.html b/pkg/blackfriday/testdata/Hard-wrapped paragraphs with list-like lines no empty line before block.html new file mode 100644 index 00000000..fc253194 --- /dev/null +++ b/pkg/blackfriday/testdata/Hard-wrapped paragraphs with list-like lines no empty line before block.html @@ -0,0 +1,14 @@ +

    In Markdown 1.0.0 and earlier. Version

    + +
      +
    1. This line turns into a list item. +Because a hard-wrapped line in the +middle of a paragraph looked like a +list item.
    2. +
    + +

    Here's one with a bullet.

    + +
      +
    • criminey.
    • +
    diff --git a/pkg/blackfriday/testdata/Hard-wrapped paragraphs with list-like lines no empty line before block.text b/pkg/blackfriday/testdata/Hard-wrapped paragraphs with list-like lines no empty line before block.text new file mode 100644 index 00000000..f8a5b27b --- /dev/null +++ b/pkg/blackfriday/testdata/Hard-wrapped paragraphs with list-like lines no empty line before block.text @@ -0,0 +1,8 @@ +In Markdown 1.0.0 and earlier. Version +8. This line turns into a list item. +Because a hard-wrapped line in the +middle of a paragraph looked like a +list item. + +Here's one with a bullet. +* criminey. diff --git a/pkg/blackfriday/testdata/Hard-wrapped paragraphs with list-like lines.html b/pkg/blackfriday/testdata/Hard-wrapped paragraphs with list-like lines.html new file mode 100644 index 00000000..e21ac79a --- /dev/null +++ b/pkg/blackfriday/testdata/Hard-wrapped paragraphs with list-like lines.html @@ -0,0 +1,8 @@ +

    In Markdown 1.0.0 and earlier. Version +8. This line turns into a list item. +Because a hard-wrapped line in the +middle of a paragraph looked like a +list item.

    + +

    Here's one with a bullet. +* criminey.

    diff --git a/pkg/blackfriday/testdata/Hard-wrapped paragraphs with list-like lines.text b/pkg/blackfriday/testdata/Hard-wrapped paragraphs with list-like lines.text new file mode 100644 index 00000000..f8a5b27b --- /dev/null +++ b/pkg/blackfriday/testdata/Hard-wrapped paragraphs with list-like lines.text @@ -0,0 +1,8 @@ +In Markdown 1.0.0 and earlier. Version +8. This line turns into a list item. +Because a hard-wrapped line in the +middle of a paragraph looked like a +list item. + +Here's one with a bullet. +* criminey. diff --git a/pkg/blackfriday/testdata/Horizontal rules.html b/pkg/blackfriday/testdata/Horizontal rules.html new file mode 100644 index 00000000..e60d4ba2 --- /dev/null +++ b/pkg/blackfriday/testdata/Horizontal rules.html @@ -0,0 +1,71 @@ +

    Dashes:

    + +
    + +
    + +
    + +
    + +
    ---
    +
    + +
    + +
    + +
    + +
    + +
    - - -
    +
    + +

    Asterisks:

    + +
    + +
    + +
    + +
    + +
    ***
    +
    + +
    + +
    + +
    + +
    + +
    * * *
    +
    + +

    Underscores:

    + +
    + +
    + +
    + +
    + +
    ___
    +
    + +
    + +
    + +
    + +
    + +
    _ _ _
    +
    diff --git a/pkg/blackfriday/testdata/Horizontal rules.text b/pkg/blackfriday/testdata/Horizontal rules.text new file mode 100644 index 00000000..1594bda2 --- /dev/null +++ b/pkg/blackfriday/testdata/Horizontal rules.text @@ -0,0 +1,67 @@ +Dashes: + +--- + + --- + + --- + + --- + + --- + +- - - + + - - - + + - - - + + - - - + + - - - + + +Asterisks: + +*** + + *** + + *** + + *** + + *** + +* * * + + * * * + + * * * + + * * * + + * * * + + +Underscores: + +___ + + ___ + + ___ + + ___ + + ___ + +_ _ _ + + _ _ _ + + _ _ _ + + _ _ _ + + _ _ _ diff --git a/pkg/blackfriday/testdata/Inline HTML (Advanced).html b/pkg/blackfriday/testdata/Inline HTML (Advanced).html new file mode 100644 index 00000000..3af9cafb --- /dev/null +++ b/pkg/blackfriday/testdata/Inline HTML (Advanced).html @@ -0,0 +1,15 @@ +

    Simple block on one line:

    + +
    foo
    + +

    And nested without indentation:

    + +
    +
    +
    +foo +
    +
    +
    +
    bar
    +
    diff --git a/pkg/blackfriday/testdata/Inline HTML (Advanced).text b/pkg/blackfriday/testdata/Inline HTML (Advanced).text new file mode 100644 index 00000000..86b7206d --- /dev/null +++ b/pkg/blackfriday/testdata/Inline HTML (Advanced).text @@ -0,0 +1,15 @@ +Simple block on one line: + +
    foo
    + +And nested without indentation: + +
    +
    +
    +foo +
    +
    +
    +
    bar
    +
    diff --git a/pkg/blackfriday/testdata/Inline HTML (Simple).html b/pkg/blackfriday/testdata/Inline HTML (Simple).html new file mode 100644 index 00000000..f84939c3 --- /dev/null +++ b/pkg/blackfriday/testdata/Inline HTML (Simple).html @@ -0,0 +1,72 @@ +

    Here's a simple block:

    + +
    + foo +
    + +

    This should be a code block, though:

    + +
    <div>
    +	foo
    +</div>
    +
    + +

    As should this:

    + +
    <div>foo</div>
    +
    + +

    Now, nested:

    + +
    +
    +
    + foo +
    +
    +
    + +

    This should just be an HTML comment:

    + + + +

    Multiline:

    + + + +

    Code block:

    + +
    <!-- Comment -->
    +
    + +

    Just plain comment, with trailing spaces on the line:

    + + + +

    Code:

    + +
    <hr />
    +
    + +

    Hr's:

    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    diff --git a/pkg/blackfriday/testdata/Inline HTML (Simple).text b/pkg/blackfriday/testdata/Inline HTML (Simple).text new file mode 100644 index 00000000..14aa2dc2 --- /dev/null +++ b/pkg/blackfriday/testdata/Inline HTML (Simple).text @@ -0,0 +1,69 @@ +Here's a simple block: + +
    + foo +
    + +This should be a code block, though: + +
    + foo +
    + +As should this: + +
    foo
    + +Now, nested: + +
    +
    +
    + foo +
    +
    +
    + +This should just be an HTML comment: + + + +Multiline: + + + +Code block: + + + +Just plain comment, with trailing spaces on the line: + + + +Code: + +
    + +Hr's: + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + diff --git a/pkg/blackfriday/testdata/Inline HTML comments.html b/pkg/blackfriday/testdata/Inline HTML comments.html new file mode 100644 index 00000000..f201242d --- /dev/null +++ b/pkg/blackfriday/testdata/Inline HTML comments.html @@ -0,0 +1,13 @@ +

    Paragraph one.

    + + + + + +

    Paragraph two.

    + + + +

    The end.

    diff --git a/pkg/blackfriday/testdata/Inline HTML comments.text b/pkg/blackfriday/testdata/Inline HTML comments.text new file mode 100644 index 00000000..41d830d0 --- /dev/null +++ b/pkg/blackfriday/testdata/Inline HTML comments.text @@ -0,0 +1,13 @@ +Paragraph one. + + + + + +Paragraph two. + + + +The end. diff --git a/pkg/blackfriday/testdata/Links, inline style.html b/pkg/blackfriday/testdata/Links, inline style.html new file mode 100644 index 00000000..5802f2de --- /dev/null +++ b/pkg/blackfriday/testdata/Links, inline style.html @@ -0,0 +1,11 @@ +

    Just a URL.

    + +

    URL and title.

    + +

    URL and title.

    + +

    URL and title.

    + +

    URL and title.

    + +

    [Empty]().

    diff --git a/pkg/blackfriday/testdata/Links, inline style.text b/pkg/blackfriday/testdata/Links, inline style.text new file mode 100644 index 00000000..09017a90 --- /dev/null +++ b/pkg/blackfriday/testdata/Links, inline style.text @@ -0,0 +1,12 @@ +Just a [URL](/url/). + +[URL and title](/url/ "title"). + +[URL and title](/url/ "title preceded by two spaces"). + +[URL and title](/url/ "title preceded by a tab"). + +[URL and title](/url/ "title has spaces afterward" ). + + +[Empty](). diff --git a/pkg/blackfriday/testdata/Links, reference style.html b/pkg/blackfriday/testdata/Links, reference style.html new file mode 100644 index 00000000..bebefdee --- /dev/null +++ b/pkg/blackfriday/testdata/Links, reference style.html @@ -0,0 +1,52 @@ +

    Foo bar.

    + +

    Foo bar.

    + +

    Foo bar.

    + +

    With embedded [brackets].

    + +

    Indented once.

    + +

    Indented twice.

    + +

    Indented thrice.

    + +

    Indented [four][] times.

    + +
    [four]: /url
    +
    + +
    + +

    this should work

    + +

    So should this.

    + +

    And this.

    + +

    And this.

    + +

    And this.

    + +

    But not [that] [].

    + +

    Nor [that][].

    + +

    Nor [that].

    + +

    [Something in brackets like this should work]

    + +

    [Same with this.]

    + +

    In this case, this points to something else.

    + +

    Backslashing should suppress [this] and [this].

    + +
    + +

    Here's one where the link +breaks across lines.

    + +

    Here's another where the link +breaks across lines, but with a line-ending space.

    diff --git a/pkg/blackfriday/testdata/Links, reference style.text b/pkg/blackfriday/testdata/Links, reference style.text new file mode 100644 index 00000000..341ec88e --- /dev/null +++ b/pkg/blackfriday/testdata/Links, reference style.text @@ -0,0 +1,71 @@ +Foo [bar] [1]. + +Foo [bar][1]. + +Foo [bar] +[1]. + +[1]: /url/ "Title" + + +With [embedded [brackets]] [b]. + + +Indented [once][]. + +Indented [twice][]. + +Indented [thrice][]. + +Indented [four][] times. + + [once]: /url + + [twice]: /url + + [thrice]: /url + + [four]: /url + + +[b]: /url/ + +* * * + +[this] [this] should work + +So should [this][this]. + +And [this] []. + +And [this][]. + +And [this]. + +But not [that] []. + +Nor [that][]. + +Nor [that]. + +[Something in brackets like [this][] should work] + +[Same with [this].] + +In this case, [this](/somethingelse/) points to something else. + +Backslashing should suppress \[this] and [this\]. + +[this]: foo + + +* * * + +Here's one where the [link +breaks] across lines. + +Here's another where the [link +breaks] across lines, but with a line-ending space. + + +[link breaks]: /url/ diff --git a/pkg/blackfriday/testdata/Links, shortcut references.html b/pkg/blackfriday/testdata/Links, shortcut references.html new file mode 100644 index 00000000..0b5e1d64 --- /dev/null +++ b/pkg/blackfriday/testdata/Links, shortcut references.html @@ -0,0 +1,9 @@ +

    This is the simple case.

    + +

    This one has a line +break.

    + +

    This one has a line +break with a line-ending space.

    + +

    this and the other

    diff --git a/pkg/blackfriday/testdata/Links, shortcut references.text b/pkg/blackfriday/testdata/Links, shortcut references.text new file mode 100644 index 00000000..8c44c98f --- /dev/null +++ b/pkg/blackfriday/testdata/Links, shortcut references.text @@ -0,0 +1,20 @@ +This is the [simple case]. + +[simple case]: /simple + + + +This one has a [line +break]. + +This one has a [line +break] with a line-ending space. + +[line break]: /foo + + +[this] [that] and the [other] + +[this]: /this +[that]: /that +[other]: /other diff --git a/pkg/blackfriday/testdata/Literal quotes in titles.html b/pkg/blackfriday/testdata/Literal quotes in titles.html new file mode 100644 index 00000000..611c1ac6 --- /dev/null +++ b/pkg/blackfriday/testdata/Literal quotes in titles.html @@ -0,0 +1,3 @@ +

    Foo bar.

    + +

    Foo bar.

    diff --git a/pkg/blackfriday/testdata/Literal quotes in titles.text b/pkg/blackfriday/testdata/Literal quotes in titles.text new file mode 100644 index 00000000..29d0e423 --- /dev/null +++ b/pkg/blackfriday/testdata/Literal quotes in titles.text @@ -0,0 +1,7 @@ +Foo [bar][]. + +Foo [bar](/url/ "Title with "quotes" inside"). + + + [bar]: /url/ "Title with "quotes" inside" + diff --git a/pkg/blackfriday/testdata/Markdown Documentation - Basics.html b/pkg/blackfriday/testdata/Markdown Documentation - Basics.html new file mode 100644 index 00000000..ea3a61c3 --- /dev/null +++ b/pkg/blackfriday/testdata/Markdown Documentation - Basics.html @@ -0,0 +1,314 @@ +

    Markdown: Basics

    + + + +

    Getting the Gist of Markdown's Formatting Syntax

    + +

    This page offers a brief overview of what it's like to use Markdown. +The syntax page provides complete, detailed documentation for +every feature, but Markdown should be very easy to pick up simply by +looking at a few examples of it in action. The examples on this page +are written in a before/after style, showing example syntax and the +HTML output produced by Markdown.

    + +

    It's also helpful to simply try Markdown out; the Dingus is a +web application that allows you type your own Markdown-formatted text +and translate it to XHTML.

    + +

    Note: This document is itself written using Markdown; you +can see the source for it by adding '.text' to the URL.

    + +

    Paragraphs, Headers, Blockquotes

    + +

    A paragraph is simply one or more consecutive lines of text, separated +by one or more blank lines. (A blank line is any line that looks like a +blank line -- a line containing nothing spaces or tabs is considered +blank.) Normal paragraphs should not be intended with spaces or tabs.

    + +

    Markdown offers two styles of headers: Setext and atx. +Setext-style headers for <h1> and <h2> are created by +"underlining" with equal signs (=) and hyphens (-), respectively. +To create an atx-style header, you put 1-6 hash marks (#) at the +beginning of the line -- the number of hashes equals the resulting +HTML header level.

    + +

    Blockquotes are indicated using email-style '>' angle brackets.

    + +

    Markdown:

    + +
    A First Level Header
    +====================
    +
    +A Second Level Header
    +---------------------
    +
    +Now is the time for all good men to come to
    +the aid of their country. This is just a
    +regular paragraph.
    +
    +The quick brown fox jumped over the lazy
    +dog's back.
    +
    +### Header 3
    +
    +> This is a blockquote.
    +> 
    +> This is the second paragraph in the blockquote.
    +>
    +> ## This is an H2 in a blockquote
    +
    + +

    Output:

    + +
    <h1>A First Level Header</h1>
    +
    +<h2>A Second Level Header</h2>
    +
    +<p>Now is the time for all good men to come to
    +the aid of their country. This is just a
    +regular paragraph.</p>
    +
    +<p>The quick brown fox jumped over the lazy
    +dog's back.</p>
    +
    +<h3>Header 3</h3>
    +
    +<blockquote>
    +    <p>This is a blockquote.</p>
    +
    +    <p>This is the second paragraph in the blockquote.</p>
    +
    +    <h2>This is an H2 in a blockquote</h2>
    +</blockquote>
    +
    + +

    Phrase Emphasis

    + +

    Markdown uses asterisks and underscores to indicate spans of emphasis.

    + +

    Markdown:

    + +
    Some of these words *are emphasized*.
    +Some of these words _are emphasized also_.
    +
    +Use two asterisks for **strong emphasis**.
    +Or, if you prefer, __use two underscores instead__.
    +
    + +

    Output:

    + +
    <p>Some of these words <em>are emphasized</em>.
    +Some of these words <em>are emphasized also</em>.</p>
    +
    +<p>Use two asterisks for <strong>strong emphasis</strong>.
    +Or, if you prefer, <strong>use two underscores instead</strong>.</p>
    +
    + +

    Lists

    + +

    Unordered (bulleted) lists use asterisks, pluses, and hyphens (*, ++, and -) as list markers. These three markers are +interchangable; this:

    + +
    *   Candy.
    +*   Gum.
    +*   Booze.
    +
    + +

    this:

    + +
    +   Candy.
    ++   Gum.
    ++   Booze.
    +
    + +

    and this:

    + +
    -   Candy.
    +-   Gum.
    +-   Booze.
    +
    + +

    all produce the same output:

    + +
    <ul>
    +<li>Candy.</li>
    +<li>Gum.</li>
    +<li>Booze.</li>
    +</ul>
    +
    + +

    Ordered (numbered) lists use regular numbers, followed by periods, as +list markers:

    + +
    1.  Red
    +2.  Green
    +3.  Blue
    +
    + +

    Output:

    + +
    <ol>
    +<li>Red</li>
    +<li>Green</li>
    +<li>Blue</li>
    +</ol>
    +
    + +

    If you put blank lines between items, you'll get <p> tags for the +list item text. You can create multi-paragraph list items by indenting +the paragraphs by 4 spaces or 1 tab:

    + +
    *   A list item.
    +
    +    With multiple paragraphs.
    +
    +*   Another item in the list.
    +
    + +

    Output:

    + +
    <ul>
    +<li><p>A list item.</p>
    +<p>With multiple paragraphs.</p></li>
    +<li><p>Another item in the list.</p></li>
    +</ul>
    +
    + +

    Links

    + +

    Markdown supports two styles for creating links: inline and +reference. With both styles, you use square brackets to delimit the +text you want to turn into a link.

    + +

    Inline-style links use parentheses immediately after the link text. +For example:

    + +
    This is an [example link](http://example.com/).
    +
    + +

    Output:

    + +
    <p>This is an <a href="http://example.com/">
    +example link</a>.</p>
    +
    + +

    Optionally, you may include a title attribute in the parentheses:

    + +
    This is an [example link](http://example.com/ "With a Title").
    +
    + +

    Output:

    + +
    <p>This is an <a href="http://example.com/" title="With a Title">
    +example link</a>.</p>
    +
    + +

    Reference-style links allow you to refer to your links by names, which +you define elsewhere in your document:

    + +
    I get 10 times more traffic from [Google][1] than from
    +[Yahoo][2] or [MSN][3].
    +
    +[1]: http://google.com/        "Google"
    +[2]: http://search.yahoo.com/  "Yahoo Search"
    +[3]: http://search.msn.com/    "MSN Search"
    +
    + +

    Output:

    + +
    <p>I get 10 times more traffic from <a href="http://google.com/"
    +title="Google">Google</a> than from <a href="http://search.yahoo.com/"
    +title="Yahoo Search">Yahoo</a> or <a href="http://search.msn.com/"
    +title="MSN Search">MSN</a>.</p>
    +
    + +

    The title attribute is optional. Link names may contain letters, +numbers and spaces, but are not case sensitive:

    + +
    I start my morning with a cup of coffee and
    +[The New York Times][NY Times].
    +
    +[ny times]: http://www.nytimes.com/
    +
    + +

    Output:

    + +
    <p>I start my morning with a cup of coffee and
    +<a href="http://www.nytimes.com/">The New York Times</a>.</p>
    +
    + +

    Images

    + +

    Image syntax is very much like link syntax.

    + +

    Inline (titles are optional):

    + +
    ![alt text](/path/to/img.jpg "Title")
    +
    + +

    Reference-style:

    + +
    ![alt text][id]
    +
    +[id]: /path/to/img.jpg "Title"
    +
    + +

    Both of the above examples produce the same output:

    + +
    <img src="/path/to/img.jpg" alt="alt text" title="Title" />
    +
    + +

    Code

    + +

    In a regular paragraph, you can create code span by wrapping text in +backtick quotes. Any ampersands (&) and angle brackets (< or +>) will automatically be translated into HTML entities. This makes +it easy to use Markdown to write about HTML example code:

    + +
    I strongly recommend against using any `<blink>` tags.
    +
    +I wish SmartyPants used named entities like `&mdash;`
    +instead of decimal-encoded entites like `&#8212;`.
    +
    + +

    Output:

    + +
    <p>I strongly recommend against using any
    +<code>&lt;blink&gt;</code> tags.</p>
    +
    +<p>I wish SmartyPants used named entities like
    +<code>&amp;mdash;</code> instead of decimal-encoded
    +entites like <code>&amp;#8212;</code>.</p>
    +
    + +

    To specify an entire block of pre-formatted code, indent every line of +the block by 4 spaces or 1 tab. Just like with code spans, &, <, +and > characters will be escaped automatically.

    + +

    Markdown:

    + +
    If you want your page to validate under XHTML 1.0 Strict,
    +you've got to put paragraph tags in your blockquotes:
    +
    +    <blockquote>
    +        <p>For example.</p>
    +    </blockquote>
    +
    + +

    Output:

    + +
    <p>If you want your page to validate under XHTML 1.0 Strict,
    +you've got to put paragraph tags in your blockquotes:</p>
    +
    +<pre><code>&lt;blockquote&gt;
    +    &lt;p&gt;For example.&lt;/p&gt;
    +&lt;/blockquote&gt;
    +</code></pre>
    +
    diff --git a/pkg/blackfriday/testdata/Markdown Documentation - Basics.text b/pkg/blackfriday/testdata/Markdown Documentation - Basics.text new file mode 100644 index 00000000..486055ca --- /dev/null +++ b/pkg/blackfriday/testdata/Markdown Documentation - Basics.text @@ -0,0 +1,306 @@ +Markdown: Basics +================ + + + + +Getting the Gist of Markdown's Formatting Syntax +------------------------------------------------ + +This page offers a brief overview of what it's like to use Markdown. +The [syntax page] [s] provides complete, detailed documentation for +every feature, but Markdown should be very easy to pick up simply by +looking at a few examples of it in action. The examples on this page +are written in a before/after style, showing example syntax and the +HTML output produced by Markdown. + +It's also helpful to simply try Markdown out; the [Dingus] [d] is a +web application that allows you type your own Markdown-formatted text +and translate it to XHTML. + +**Note:** This document is itself written using Markdown; you +can [see the source for it by adding '.text' to the URL] [src]. + + [s]: /projects/markdown/syntax "Markdown Syntax" + [d]: /projects/markdown/dingus "Markdown Dingus" + [src]: /projects/markdown/basics.text + + +## Paragraphs, Headers, Blockquotes ## + +A paragraph is simply one or more consecutive lines of text, separated +by one or more blank lines. (A blank line is any line that looks like a +blank line -- a line containing nothing spaces or tabs is considered +blank.) Normal paragraphs should not be intended with spaces or tabs. + +Markdown offers two styles of headers: *Setext* and *atx*. +Setext-style headers for `

    ` and `

    ` are created by +"underlining" with equal signs (`=`) and hyphens (`-`), respectively. +To create an atx-style header, you put 1-6 hash marks (`#`) at the +beginning of the line -- the number of hashes equals the resulting +HTML header level. + +Blockquotes are indicated using email-style '`>`' angle brackets. + +Markdown: + + A First Level Header + ==================== + + A Second Level Header + --------------------- + + Now is the time for all good men to come to + the aid of their country. This is just a + regular paragraph. + + The quick brown fox jumped over the lazy + dog's back. + + ### Header 3 + + > This is a blockquote. + > + > This is the second paragraph in the blockquote. + > + > ## This is an H2 in a blockquote + + +Output: + +

    A First Level Header

    + +

    A Second Level Header

    + +

    Now is the time for all good men to come to + the aid of their country. This is just a + regular paragraph.

    + +

    The quick brown fox jumped over the lazy + dog's back.

    + +

    Header 3

    + +
    +

    This is a blockquote.

    + +

    This is the second paragraph in the blockquote.

    + +

    This is an H2 in a blockquote

    +
    + + + +### Phrase Emphasis ### + +Markdown uses asterisks and underscores to indicate spans of emphasis. + +Markdown: + + Some of these words *are emphasized*. + Some of these words _are emphasized also_. + + Use two asterisks for **strong emphasis**. + Or, if you prefer, __use two underscores instead__. + +Output: + +

    Some of these words are emphasized. + Some of these words are emphasized also.

    + +

    Use two asterisks for strong emphasis. + Or, if you prefer, use two underscores instead.

    + + + +## Lists ## + +Unordered (bulleted) lists use asterisks, pluses, and hyphens (`*`, +`+`, and `-`) as list markers. These three markers are +interchangable; this: + + * Candy. + * Gum. + * Booze. + +this: + + + Candy. + + Gum. + + Booze. + +and this: + + - Candy. + - Gum. + - Booze. + +all produce the same output: + +
      +
    • Candy.
    • +
    • Gum.
    • +
    • Booze.
    • +
    + +Ordered (numbered) lists use regular numbers, followed by periods, as +list markers: + + 1. Red + 2. Green + 3. Blue + +Output: + +
      +
    1. Red
    2. +
    3. Green
    4. +
    5. Blue
    6. +
    + +If you put blank lines between items, you'll get `

    ` tags for the +list item text. You can create multi-paragraph list items by indenting +the paragraphs by 4 spaces or 1 tab: + + * A list item. + + With multiple paragraphs. + + * Another item in the list. + +Output: + +

      +
    • A list item.

      +

      With multiple paragraphs.

    • +
    • Another item in the list.

    • +
    + + + +### Links ### + +Markdown supports two styles for creating links: *inline* and +*reference*. With both styles, you use square brackets to delimit the +text you want to turn into a link. + +Inline-style links use parentheses immediately after the link text. +For example: + + This is an [example link](http://example.com/). + +Output: + +

    This is an + example link.

    + +Optionally, you may include a title attribute in the parentheses: + + This is an [example link](http://example.com/ "With a Title"). + +Output: + +

    This is an + example link.

    + +Reference-style links allow you to refer to your links by names, which +you define elsewhere in your document: + + I get 10 times more traffic from [Google][1] than from + [Yahoo][2] or [MSN][3]. + + [1]: http://google.com/ "Google" + [2]: http://search.yahoo.com/ "Yahoo Search" + [3]: http://search.msn.com/ "MSN Search" + +Output: + +

    I get 10 times more traffic from Google than from Yahoo or MSN.

    + +The title attribute is optional. Link names may contain letters, +numbers and spaces, but are *not* case sensitive: + + I start my morning with a cup of coffee and + [The New York Times][NY Times]. + + [ny times]: http://www.nytimes.com/ + +Output: + +

    I start my morning with a cup of coffee and + The New York Times.

    + + +### Images ### + +Image syntax is very much like link syntax. + +Inline (titles are optional): + + ![alt text](/path/to/img.jpg "Title") + +Reference-style: + + ![alt text][id] + + [id]: /path/to/img.jpg "Title" + +Both of the above examples produce the same output: + + alt text + + + +### Code ### + +In a regular paragraph, you can create code span by wrapping text in +backtick quotes. Any ampersands (`&`) and angle brackets (`<` or +`>`) will automatically be translated into HTML entities. This makes +it easy to use Markdown to write about HTML example code: + + I strongly recommend against using any `` tags. + + I wish SmartyPants used named entities like `—` + instead of decimal-encoded entites like `—`. + +Output: + +

    I strongly recommend against using any + <blink> tags.

    + +

    I wish SmartyPants used named entities like + &mdash; instead of decimal-encoded + entites like &#8212;.

    + + +To specify an entire block of pre-formatted code, indent every line of +the block by 4 spaces or 1 tab. Just like with code spans, `&`, `<`, +and `>` characters will be escaped automatically. + +Markdown: + + If you want your page to validate under XHTML 1.0 Strict, + you've got to put paragraph tags in your blockquotes: + +
    +

    For example.

    +
    + +Output: + +

    If you want your page to validate under XHTML 1.0 Strict, + you've got to put paragraph tags in your blockquotes:

    + +
    <blockquote>
    +        <p>For example.</p>
    +    </blockquote>
    +    
    diff --git a/pkg/blackfriday/testdata/Markdown Documentation - Syntax.html b/pkg/blackfriday/testdata/Markdown Documentation - Syntax.html new file mode 100644 index 00000000..6cd05fb9 --- /dev/null +++ b/pkg/blackfriday/testdata/Markdown Documentation - Syntax.html @@ -0,0 +1,946 @@ +

    Markdown: Syntax

    + + + + + +

    Note: This document is itself written using Markdown; you +can see the source for it by adding '.text' to the URL.

    + +
    + +

    Overview

    + +

    Philosophy

    + +

    Markdown is intended to be as easy-to-read and easy-to-write as is feasible.

    + +

    Readability, however, is emphasized above all else. A Markdown-formatted +document should be publishable as-is, as plain text, without looking +like it's been marked up with tags or formatting instructions. While +Markdown's syntax has been influenced by several existing text-to-HTML +filters -- including Setext, atx, Textile, reStructuredText, +Grutatext, and EtText -- the single biggest source of +inspiration for Markdown's syntax is the format of plain text email.

    + +

    To this end, Markdown's syntax is comprised entirely of punctuation +characters, which punctuation characters have been carefully chosen so +as to look like what they mean. E.g., asterisks around a word actually +look like *emphasis*. Markdown lists look like, well, lists. Even +blockquotes look like quoted passages of text, assuming you've ever +used email.

    + +

    Inline HTML

    + +

    Markdown's syntax is intended for one purpose: to be used as a +format for writing for the web.

    + +

    Markdown is not a replacement for HTML, or even close to it. Its +syntax is very small, corresponding only to a very small subset of +HTML tags. The idea is not to create a syntax that makes it easier +to insert HTML tags. In my opinion, HTML tags are already easy to +insert. The idea for Markdown is to make it easy to read, write, and +edit prose. HTML is a publishing format; Markdown is a writing +format. Thus, Markdown's formatting syntax only addresses issues that +can be conveyed in plain text.

    + +

    For any markup that is not covered by Markdown's syntax, you simply +use HTML itself. There's no need to preface it or delimit it to +indicate that you're switching from Markdown to HTML; you just use +the tags.

    + +

    The only restrictions are that block-level HTML elements -- e.g. <div>, +<table>, <pre>, <p>, etc. -- must be separated from surrounding +content by blank lines, and the start and end tags of the block should +not be indented with tabs or spaces. Markdown is smart enough not +to add extra (unwanted) <p> tags around HTML block-level tags.

    + +

    For example, to add an HTML table to a Markdown article:

    + +
    This is a regular paragraph.
    +
    +<table>
    +    <tr>
    +        <td>Foo</td>
    +    </tr>
    +</table>
    +
    +This is another regular paragraph.
    +
    + +

    Note that Markdown formatting syntax is not processed within block-level +HTML tags. E.g., you can't use Markdown-style *emphasis* inside an +HTML block.

    + +

    Span-level HTML tags -- e.g. <span>, <cite>, or <del> -- can be +used anywhere in a Markdown paragraph, list item, or header. If you +want, you can even use HTML tags instead of Markdown formatting; e.g. if +you'd prefer to use HTML <a> or <img> tags instead of Markdown's +link or image syntax, go right ahead.

    + +

    Unlike block-level HTML tags, Markdown syntax is processed within +span-level tags.

    + +

    Automatic Escaping for Special Characters

    + +

    In HTML, there are two characters that demand special treatment: < +and &. Left angle brackets are used to start tags; ampersands are +used to denote HTML entities. If you want to use them as literal +characters, you must escape them as entities, e.g. &lt;, and +&amp;.

    + +

    Ampersands in particular are bedeviling for web writers. If you want to +write about 'AT&T', you need to write 'AT&amp;T'. You even need to +escape ampersands within URLs. Thus, if you want to link to:

    + +
    http://images.google.com/images?num=30&q=larry+bird
    +
    + +

    you need to encode the URL as:

    + +
    http://images.google.com/images?num=30&amp;q=larry+bird
    +
    + +

    in your anchor tag href attribute. Needless to say, this is easy to +forget, and is probably the single most common source of HTML validation +errors in otherwise well-marked-up web sites.

    + +

    Markdown allows you to use these characters naturally, taking care of +all the necessary escaping for you. If you use an ampersand as part of +an HTML entity, it remains unchanged; otherwise it will be translated +into &amp;.

    + +

    So, if you want to include a copyright symbol in your article, you can write:

    + +
    &copy;
    +
    + +

    and Markdown will leave it alone. But if you write:

    + +
    AT&T
    +
    + +

    Markdown will translate it to:

    + +
    AT&amp;T
    +
    + +

    Similarly, because Markdown supports inline HTML, if you use +angle brackets as delimiters for HTML tags, Markdown will treat them as +such. But if you write:

    + +
    4 < 5
    +
    + +

    Markdown will translate it to:

    + +
    4 &lt; 5
    +
    + +

    However, inside Markdown code spans and blocks, angle brackets and +ampersands are always encoded automatically. This makes it easy to use +Markdown to write about HTML code. (As opposed to raw HTML, which is a +terrible format for writing about HTML syntax, because every single < +and & in your example code needs to be escaped.)

    + +
    + +

    Block Elements

    + +

    Paragraphs and Line Breaks

    + +

    A paragraph is simply one or more consecutive lines of text, separated +by one or more blank lines. (A blank line is any line that looks like a +blank line -- a line containing nothing but spaces or tabs is considered +blank.) Normal paragraphs should not be intended with spaces or tabs.

    + +

    The implication of the "one or more consecutive lines of text" rule is +that Markdown supports "hard-wrapped" text paragraphs. This differs +significantly from most other text-to-HTML formatters (including Movable +Type's "Convert Line Breaks" option) which translate every line break +character in a paragraph into a <br /> tag.

    + +

    When you do want to insert a <br /> break tag using Markdown, you +end a line with two or more spaces, then type return.

    + +

    Yes, this takes a tad more effort to create a <br />, but a simplistic +"every line break is a <br />" rule wouldn't work for Markdown. +Markdown's email-style blockquoting and multi-paragraph list items +work best -- and look better -- when you format them with hard breaks.

    + + + +

    Markdown supports two styles of headers, Setext and atx.

    + +

    Setext-style headers are "underlined" using equal signs (for first-level +headers) and dashes (for second-level headers). For example:

    + +
    This is an H1
    +=============
    +
    +This is an H2
    +-------------
    +
    + +

    Any number of underlining ='s or -'s will work.

    + +

    Atx-style headers use 1-6 hash characters at the start of the line, +corresponding to header levels 1-6. For example:

    + +
    # This is an H1
    +
    +## This is an H2
    +
    +###### This is an H6
    +
    + +

    Optionally, you may "close" atx-style headers. This is purely +cosmetic -- you can use this if you think it looks better. The +closing hashes don't even need to match the number of hashes +used to open the header. (The number of opening hashes +determines the header level.) :

    + +
    # This is an H1 #
    +
    +## This is an H2 ##
    +
    +### This is an H3 ######
    +
    + +

    Blockquotes

    + +

    Markdown uses email-style > characters for blockquoting. If you're +familiar with quoting passages of text in an email message, then you +know how to create a blockquote in Markdown. It looks best if you hard +wrap the text and put a > before every line:

    + +
    > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
    +> consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
    +> Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
    +> 
    +> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
    +> id sem consectetuer libero luctus adipiscing.
    +
    + +

    Markdown allows you to be lazy and only put the > before the first +line of a hard-wrapped paragraph:

    + +
    > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
    +consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
    +Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
    +
    +> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
    +id sem consectetuer libero luctus adipiscing.
    +
    + +

    Blockquotes can be nested (i.e. a blockquote-in-a-blockquote) by +adding additional levels of >:

    + +
    > This is the first level of quoting.
    +>
    +> > This is nested blockquote.
    +>
    +> Back to the first level.
    +
    + +

    Blockquotes can contain other Markdown elements, including headers, lists, +and code blocks:

    + +
    > ## This is a header.
    +> 
    +> 1.   This is the first list item.
    +> 2.   This is the second list item.
    +> 
    +> Here's some example code:
    +> 
    +>     return shell_exec("echo $input | $markdown_script");
    +
    + +

    Any decent text editor should make email-style quoting easy. For +example, with BBEdit, you can make a selection and choose Increase +Quote Level from the Text menu.

    + +

    Lists

    + +

    Markdown supports ordered (numbered) and unordered (bulleted) lists.

    + +

    Unordered lists use asterisks, pluses, and hyphens -- interchangably +-- as list markers:

    + +
    *   Red
    +*   Green
    +*   Blue
    +
    + +

    is equivalent to:

    + +
    +   Red
    ++   Green
    ++   Blue
    +
    + +

    and:

    + +
    -   Red
    +-   Green
    +-   Blue
    +
    + +

    Ordered lists use numbers followed by periods:

    + +
    1.  Bird
    +2.  McHale
    +3.  Parish
    +
    + +

    It's important to note that the actual numbers you use to mark the +list have no effect on the HTML output Markdown produces. The HTML +Markdown produces from the above list is:

    + +
    <ol>
    +<li>Bird</li>
    +<li>McHale</li>
    +<li>Parish</li>
    +</ol>
    +
    + +

    If you instead wrote the list in Markdown like this:

    + +
    1.  Bird
    +1.  McHale
    +1.  Parish
    +
    + +

    or even:

    + +
    3. Bird
    +1. McHale
    +8. Parish
    +
    + +

    you'd get the exact same HTML output. The point is, if you want to, +you can use ordinal numbers in your ordered Markdown lists, so that +the numbers in your source match the numbers in your published HTML. +But if you want to be lazy, you don't have to.

    + +

    If you do use lazy list numbering, however, you should still start the +list with the number 1. At some point in the future, Markdown may support +starting ordered lists at an arbitrary number.

    + +

    List markers typically start at the left margin, but may be indented by +up to three spaces. List markers must be followed by one or more spaces +or a tab.

    + +

    To make lists look nice, you can wrap items with hanging indents:

    + +
    *   Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
    +    Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,
    +    viverra nec, fringilla in, laoreet vitae, risus.
    +*   Donec sit amet nisl. Aliquam semper ipsum sit amet velit.
    +    Suspendisse id sem consectetuer libero luctus adipiscing.
    +
    + +

    But if you want to be lazy, you don't have to:

    + +
    *   Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
    +Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,
    +viverra nec, fringilla in, laoreet vitae, risus.
    +*   Donec sit amet nisl. Aliquam semper ipsum sit amet velit.
    +Suspendisse id sem consectetuer libero luctus adipiscing.
    +
    + +

    If list items are separated by blank lines, Markdown will wrap the +items in <p> tags in the HTML output. For example, this input:

    + +
    *   Bird
    +*   Magic
    +
    + +

    will turn into:

    + +
    <ul>
    +<li>Bird</li>
    +<li>Magic</li>
    +</ul>
    +
    + +

    But this:

    + +
    *   Bird
    +
    +*   Magic
    +
    + +

    will turn into:

    + +
    <ul>
    +<li><p>Bird</p></li>
    +<li><p>Magic</p></li>
    +</ul>
    +
    + +

    List items may consist of multiple paragraphs. Each subsequent +paragraph in a list item must be intended by either 4 spaces +or one tab:

    + +
    1.  This is a list item with two paragraphs. Lorem ipsum dolor
    +    sit amet, consectetuer adipiscing elit. Aliquam hendrerit
    +    mi posuere lectus.
    +
    +    Vestibulum enim wisi, viverra nec, fringilla in, laoreet
    +    vitae, risus. Donec sit amet nisl. Aliquam semper ipsum
    +    sit amet velit.
    +
    +2.  Suspendisse id sem consectetuer libero luctus adipiscing.
    +
    + +

    It looks nice if you indent every line of the subsequent +paragraphs, but here again, Markdown will allow you to be +lazy:

    + +
    *   This is a list item with two paragraphs.
    +
    +    This is the second paragraph in the list item. You're
    +only required to indent the first line. Lorem ipsum dolor
    +sit amet, consectetuer adipiscing elit.
    +
    +*   Another item in the same list.
    +
    + +

    To put a blockquote within a list item, the blockquote's > +delimiters need to be indented:

    + +
    *   A list item with a blockquote:
    +
    +    > This is a blockquote
    +    > inside a list item.
    +
    + +

    To put a code block within a list item, the code block needs +to be indented twice -- 8 spaces or two tabs:

    + +
    *   A list item with a code block:
    +
    +        <code goes here>
    +
    + +

    It's worth noting that it's possible to trigger an ordered list by +accident, by writing something like this:

    + +
    1986. What a great season.
    +
    + +

    In other words, a number-period-space sequence at the beginning of a +line. To avoid this, you can backslash-escape the period:

    + +
    1986\. What a great season.
    +
    + +

    Code Blocks

    + +

    Pre-formatted code blocks are used for writing about programming or +markup source code. Rather than forming normal paragraphs, the lines +of a code block are interpreted literally. Markdown wraps a code block +in both <pre> and <code> tags.

    + +

    To produce a code block in Markdown, simply indent every line of the +block by at least 4 spaces or 1 tab. For example, given this input:

    + +
    This is a normal paragraph:
    +
    +    This is a code block.
    +
    + +

    Markdown will generate:

    + +
    <p>This is a normal paragraph:</p>
    +
    +<pre><code>This is a code block.
    +</code></pre>
    +
    + +

    One level of indentation -- 4 spaces or 1 tab -- is removed from each +line of the code block. For example, this:

    + +
    Here is an example of AppleScript:
    +
    +    tell application "Foo"
    +        beep
    +    end tell
    +
    + +

    will turn into:

    + +
    <p>Here is an example of AppleScript:</p>
    +
    +<pre><code>tell application "Foo"
    +    beep
    +end tell
    +</code></pre>
    +
    + +

    A code block continues until it reaches a line that is not indented +(or the end of the article).

    + +

    Within a code block, ampersands (&) and angle brackets (< and >) +are automatically converted into HTML entities. This makes it very +easy to include example HTML source code using Markdown -- just paste +it and indent it, and Markdown will handle the hassle of encoding the +ampersands and angle brackets. For example, this:

    + +
        <div class="footer">
    +        &copy; 2004 Foo Corporation
    +    </div>
    +
    + +

    will turn into:

    + +
    <pre><code>&lt;div class="footer"&gt;
    +    &amp;copy; 2004 Foo Corporation
    +&lt;/div&gt;
    +</code></pre>
    +
    + +

    Regular Markdown syntax is not processed within code blocks. E.g., +asterisks are just literal asterisks within a code block. This means +it's also easy to use Markdown to write about Markdown's own syntax.

    + +

    Horizontal Rules

    + +

    You can produce a horizontal rule tag (<hr />) by placing three or +more hyphens, asterisks, or underscores on a line by themselves. If you +wish, you may use spaces between the hyphens or asterisks. Each of the +following lines will produce a horizontal rule:

    + +
    * * *
    +
    +***
    +
    +*****
    +
    +- - -
    +
    +---------------------------------------
    +
    +_ _ _
    +
    + +
    + +

    Span Elements

    + + + +

    Markdown supports two style of links: inline and reference.

    + +

    In both styles, the link text is delimited by [square brackets].

    + +

    To create an inline link, use a set of regular parentheses immediately +after the link text's closing square bracket. Inside the parentheses, +put the URL where you want the link to point, along with an optional +title for the link, surrounded in quotes. For example:

    + +
    This is [an example](http://example.com/ "Title") inline link.
    +
    +[This link](http://example.net/) has no title attribute.
    +
    + +

    Will produce:

    + +
    <p>This is <a href="http://example.com/" title="Title">
    +an example</a> inline link.</p>
    +
    +<p><a href="http://example.net/">This link</a> has no
    +title attribute.</p>
    +
    + +

    If you're referring to a local resource on the same server, you can +use relative paths:

    + +
    See my [About](/about/) page for details.
    +
    + +

    Reference-style links use a second set of square brackets, inside +which you place a label of your choosing to identify the link:

    + +
    This is [an example][id] reference-style link.
    +
    + +

    You can optionally use a space to separate the sets of brackets:

    + +
    This is [an example] [id] reference-style link.
    +
    + +

    Then, anywhere in the document, you define your link label like this, +on a line by itself:

    + +
    [id]: http://example.com/  "Optional Title Here"
    +
    + +

    That is:

    + +
      +
    • Square brackets containing the link identifier (optionally +indented from the left margin using up to three spaces);
    • +
    • followed by a colon;
    • +
    • followed by one or more spaces (or tabs);
    • +
    • followed by the URL for the link;
    • +
    • optionally followed by a title attribute for the link, enclosed +in double or single quotes.
    • +
    + +

    The link URL may, optionally, be surrounded by angle brackets:

    + +
    [id]: <http://example.com/>  "Optional Title Here"
    +
    + +

    You can put the title attribute on the next line and use extra spaces +or tabs for padding, which tends to look better with longer URLs:

    + +
    [id]: http://example.com/longish/path/to/resource/here
    +    "Optional Title Here"
    +
    + +

    Link definitions are only used for creating links during Markdown +processing, and are stripped from your document in the HTML output.

    + +

    Link definition names may constist of letters, numbers, spaces, and punctuation -- but they are not case sensitive. E.g. these two links:

    + +
    [link text][a]
    +[link text][A]
    +
    + +

    are equivalent.

    + +

    The implicit link name shortcut allows you to omit the name of the +link, in which case the link text itself is used as the name. +Just use an empty set of square brackets -- e.g., to link the word +"Google" to the google.com web site, you could simply write:

    + +
    [Google][]
    +
    + +

    And then define the link:

    + +
    [Google]: http://google.com/
    +
    + +

    Because link names may contain spaces, this shortcut even works for +multiple words in the link text:

    + +
    Visit [Daring Fireball][] for more information.
    +
    + +

    And then define the link:

    + +
    [Daring Fireball]: http://daringfireball.net/
    +
    + +

    Link definitions can be placed anywhere in your Markdown document. I +tend to put them immediately after each paragraph in which they're +used, but if you want, you can put them all at the end of your +document, sort of like footnotes.

    + +

    Here's an example of reference links in action:

    + +
    I get 10 times more traffic from [Google] [1] than from
    +[Yahoo] [2] or [MSN] [3].
    +
    +  [1]: http://google.com/        "Google"
    +  [2]: http://search.yahoo.com/  "Yahoo Search"
    +  [3]: http://search.msn.com/    "MSN Search"
    +
    + +

    Using the implicit link name shortcut, you could instead write:

    + +
    I get 10 times more traffic from [Google][] than from
    +[Yahoo][] or [MSN][].
    +
    +  [google]: http://google.com/        "Google"
    +  [yahoo]:  http://search.yahoo.com/  "Yahoo Search"
    +  [msn]:    http://search.msn.com/    "MSN Search"
    +
    + +

    Both of the above examples will produce the following HTML output:

    + +
    <p>I get 10 times more traffic from <a href="http://google.com/"
    +title="Google">Google</a> than from
    +<a href="http://search.yahoo.com/" title="Yahoo Search">Yahoo</a>
    +or <a href="http://search.msn.com/" title="MSN Search">MSN</a>.</p>
    +
    + +

    For comparison, here is the same paragraph written using +Markdown's inline link style:

    + +
    I get 10 times more traffic from [Google](http://google.com/ "Google")
    +than from [Yahoo](http://search.yahoo.com/ "Yahoo Search") or
    +[MSN](http://search.msn.com/ "MSN Search").
    +
    + +

    The point of reference-style links is not that they're easier to +write. The point is that with reference-style links, your document +source is vastly more readable. Compare the above examples: using +reference-style links, the paragraph itself is only 81 characters +long; with inline-style links, it's 176 characters; and as raw HTML, +it's 234 characters. In the raw HTML, there's more markup than there +is text.

    + +

    With Markdown's reference-style links, a source document much more +closely resembles the final output, as rendered in a browser. By +allowing you to move the markup-related metadata out of the paragraph, +you can add links without interrupting the narrative flow of your +prose.

    + +

    Emphasis

    + +

    Markdown treats asterisks (*) and underscores (_) as indicators of +emphasis. Text wrapped with one * or _ will be wrapped with an +HTML <em> tag; double *'s or _'s will be wrapped with an HTML +<strong> tag. E.g., this input:

    + +
    *single asterisks*
    +
    +_single underscores_
    +
    +**double asterisks**
    +
    +__double underscores__
    +
    + +

    will produce:

    + +
    <em>single asterisks</em>
    +
    +<em>single underscores</em>
    +
    +<strong>double asterisks</strong>
    +
    +<strong>double underscores</strong>
    +
    + +

    You can use whichever style you prefer; the lone restriction is that +the same character must be used to open and close an emphasis span.

    + +

    Emphasis can be used in the middle of a word:

    + +
    un*fucking*believable
    +
    + +

    But if you surround an * or _ with spaces, it'll be treated as a +literal asterisk or underscore.

    + +

    To produce a literal asterisk or underscore at a position where it +would otherwise be used as an emphasis delimiter, you can backslash +escape it:

    + +
    \*this text is surrounded by literal asterisks\*
    +
    + +

    Code

    + +

    To indicate a span of code, wrap it with backtick quotes (`). +Unlike a pre-formatted code block, a code span indicates code within a +normal paragraph. For example:

    + +
    Use the `printf()` function.
    +
    + +

    will produce:

    + +
    <p>Use the <code>printf()</code> function.</p>
    +
    + +

    To include a literal backtick character within a code span, you can use +multiple backticks as the opening and closing delimiters:

    + +
    ``There is a literal backtick (`) here.``
    +
    + +

    which will produce this:

    + +
    <p><code>There is a literal backtick (`) here.</code></p>
    +
    + +

    The backtick delimiters surrounding a code span may include spaces -- +one after the opening, one before the closing. This allows you to place +literal backtick characters at the beginning or end of a code span:

    + +
    A single backtick in a code span: `` ` ``
    +
    +A backtick-delimited string in a code span: `` `foo` ``
    +
    + +

    will produce:

    + +
    <p>A single backtick in a code span: <code>`</code></p>
    +
    +<p>A backtick-delimited string in a code span: <code>`foo`</code></p>
    +
    + +

    With a code span, ampersands and angle brackets are encoded as HTML +entities automatically, which makes it easy to include example HTML +tags. Markdown will turn this:

    + +
    Please don't use any `<blink>` tags.
    +
    + +

    into:

    + +
    <p>Please don't use any <code>&lt;blink&gt;</code> tags.</p>
    +
    + +

    You can write this:

    + +
    `&#8212;` is the decimal-encoded equivalent of `&mdash;`.
    +
    + +

    to produce:

    + +
    <p><code>&amp;#8212;</code> is the decimal-encoded
    +equivalent of <code>&amp;mdash;</code>.</p>
    +
    + +

    Images

    + +

    Admittedly, it's fairly difficult to devise a "natural" syntax for +placing images into a plain text document format.

    + +

    Markdown uses an image syntax that is intended to resemble the syntax +for links, allowing for two styles: inline and reference.

    + +

    Inline image syntax looks like this:

    + +
    ![Alt text](/path/to/img.jpg)
    +
    +![Alt text](/path/to/img.jpg "Optional title")
    +
    + +

    That is:

    + +
      +
    • An exclamation mark: !;
    • +
    • followed by a set of square brackets, containing the alt +attribute text for the image;
    • +
    • followed by a set of parentheses, containing the URL or path to +the image, and an optional title attribute enclosed in double +or single quotes.
    • +
    + +

    Reference-style image syntax looks like this:

    + +
    ![Alt text][id]
    +
    + +

    Where "id" is the name of a defined image reference. Image references +are defined using syntax identical to link references:

    + +
    [id]: url/to/image  "Optional title attribute"
    +
    + +

    As of this writing, Markdown has no syntax for specifying the +dimensions of an image; if this is important to you, you can simply +use regular HTML <img> tags.

    + +
    + +

    Miscellaneous

    + + + +

    Markdown supports a shortcut style for creating "automatic" links for URLs and email addresses: simply surround the URL or email address with angle brackets. What this means is that if you want to show the actual text of a URL or email address, and also have it be a clickable link, you can do this:

    + +
    <http://example.com/>
    +
    + +

    Markdown will turn this into:

    + +
    <a href="http://example.com/">http://example.com/</a>
    +
    + +

    Automatic links for email addresses work similarly, except that +Markdown will also perform a bit of randomized decimal and hex +entity-encoding to help obscure your address from address-harvesting +spambots. For example, Markdown will turn this:

    + +
    <address@example.com>
    +
    + +

    into something like this:

    + +
    <a href="&#x6D;&#x61;i&#x6C;&#x74;&#x6F;:&#x61;&#x64;&#x64;&#x72;&#x65;
    +&#115;&#115;&#64;&#101;&#120;&#x61;&#109;&#x70;&#x6C;e&#x2E;&#99;&#111;
    +&#109;">&#x61;&#x64;&#x64;&#x72;&#x65;&#115;&#115;&#64;&#101;&#120;&#x61;
    +&#109;&#x70;&#x6C;e&#x2E;&#99;&#111;&#109;</a>
    +
    + +

    which will render in a browser as a clickable link to "address@example.com".

    + +

    (This sort of entity-encoding trick will indeed fool many, if not +most, address-harvesting bots, but it definitely won't fool all of +them. It's better than nothing, but an address published in this way +will probably eventually start receiving spam.)

    + +

    Backslash Escapes

    + +

    Markdown allows you to use backslash escapes to generate literal +characters which would otherwise have special meaning in Markdown's +formatting syntax. For example, if you wanted to surround a word with +literal asterisks (instead of an HTML <em> tag), you can backslashes +before the asterisks, like this:

    + +
    \*literal asterisks\*
    +
    + +

    Markdown provides backslash escapes for the following characters:

    + +
    \   backslash
    +`   backtick
    +*   asterisk
    +_   underscore
    +{}  curly braces
    +[]  square brackets
    +()  parentheses
    +#   hash mark
    ++	plus sign
    +-	minus sign (hyphen)
    +.   dot
    +!   exclamation mark
    +
    diff --git a/pkg/blackfriday/testdata/Markdown Documentation - Syntax.text b/pkg/blackfriday/testdata/Markdown Documentation - Syntax.text new file mode 100644 index 00000000..57360a16 --- /dev/null +++ b/pkg/blackfriday/testdata/Markdown Documentation - Syntax.text @@ -0,0 +1,888 @@ +Markdown: Syntax +================ + + + + +* [Overview](#overview) + * [Philosophy](#philosophy) + * [Inline HTML](#html) + * [Automatic Escaping for Special Characters](#autoescape) +* [Block Elements](#block) + * [Paragraphs and Line Breaks](#p) + * [Headers](#header) + * [Blockquotes](#blockquote) + * [Lists](#list) + * [Code Blocks](#precode) + * [Horizontal Rules](#hr) +* [Span Elements](#span) + * [Links](#link) + * [Emphasis](#em) + * [Code](#code) + * [Images](#img) +* [Miscellaneous](#misc) + * [Backslash Escapes](#backslash) + * [Automatic Links](#autolink) + + +**Note:** This document is itself written using Markdown; you +can [see the source for it by adding '.text' to the URL][src]. + + [src]: /projects/markdown/syntax.text + +* * * + +

    Overview

    + +

    Philosophy

    + +Markdown is intended to be as easy-to-read and easy-to-write as is feasible. + +Readability, however, is emphasized above all else. A Markdown-formatted +document should be publishable as-is, as plain text, without looking +like it's been marked up with tags or formatting instructions. While +Markdown's syntax has been influenced by several existing text-to-HTML +filters -- including [Setext] [1], [atx] [2], [Textile] [3], [reStructuredText] [4], +[Grutatext] [5], and [EtText] [6] -- the single biggest source of +inspiration for Markdown's syntax is the format of plain text email. + + [1]: http://docutils.sourceforge.net/mirror/setext.html + [2]: http://www.aaronsw.com/2002/atx/ + [3]: http://textism.com/tools/textile/ + [4]: http://docutils.sourceforge.net/rst.html + [5]: http://www.triptico.com/software/grutatxt.html + [6]: http://ettext.taint.org/doc/ + +To this end, Markdown's syntax is comprised entirely of punctuation +characters, which punctuation characters have been carefully chosen so +as to look like what they mean. E.g., asterisks around a word actually +look like \*emphasis\*. Markdown lists look like, well, lists. Even +blockquotes look like quoted passages of text, assuming you've ever +used email. + + + +

    Inline HTML

    + +Markdown's syntax is intended for one purpose: to be used as a +format for *writing* for the web. + +Markdown is not a replacement for HTML, or even close to it. Its +syntax is very small, corresponding only to a very small subset of +HTML tags. The idea is *not* to create a syntax that makes it easier +to insert HTML tags. In my opinion, HTML tags are already easy to +insert. The idea for Markdown is to make it easy to read, write, and +edit prose. HTML is a *publishing* format; Markdown is a *writing* +format. Thus, Markdown's formatting syntax only addresses issues that +can be conveyed in plain text. + +For any markup that is not covered by Markdown's syntax, you simply +use HTML itself. There's no need to preface it or delimit it to +indicate that you're switching from Markdown to HTML; you just use +the tags. + +The only restrictions are that block-level HTML elements -- e.g. `
    `, +``, `
    `, `

    `, etc. -- must be separated from surrounding +content by blank lines, and the start and end tags of the block should +not be indented with tabs or spaces. Markdown is smart enough not +to add extra (unwanted) `

    ` tags around HTML block-level tags. + +For example, to add an HTML table to a Markdown article: + + This is a regular paragraph. + +

    + + + +
    Foo
    + + This is another regular paragraph. + +Note that Markdown formatting syntax is not processed within block-level +HTML tags. E.g., you can't use Markdown-style `*emphasis*` inside an +HTML block. + +Span-level HTML tags -- e.g. ``, ``, or `` -- can be +used anywhere in a Markdown paragraph, list item, or header. If you +want, you can even use HTML tags instead of Markdown formatting; e.g. if +you'd prefer to use HTML `` or `` tags instead of Markdown's +link or image syntax, go right ahead. + +Unlike block-level HTML tags, Markdown syntax *is* processed within +span-level tags. + + +

    Automatic Escaping for Special Characters

    + +In HTML, there are two characters that demand special treatment: `<` +and `&`. Left angle brackets are used to start tags; ampersands are +used to denote HTML entities. If you want to use them as literal +characters, you must escape them as entities, e.g. `<`, and +`&`. + +Ampersands in particular are bedeviling for web writers. If you want to +write about 'AT&T', you need to write '`AT&T`'. You even need to +escape ampersands within URLs. Thus, if you want to link to: + + http://images.google.com/images?num=30&q=larry+bird + +you need to encode the URL as: + + http://images.google.com/images?num=30&q=larry+bird + +in your anchor tag `href` attribute. Needless to say, this is easy to +forget, and is probably the single most common source of HTML validation +errors in otherwise well-marked-up web sites. + +Markdown allows you to use these characters naturally, taking care of +all the necessary escaping for you. If you use an ampersand as part of +an HTML entity, it remains unchanged; otherwise it will be translated +into `&`. + +So, if you want to include a copyright symbol in your article, you can write: + + © + +and Markdown will leave it alone. But if you write: + + AT&T + +Markdown will translate it to: + + AT&T + +Similarly, because Markdown supports [inline HTML](#html), if you use +angle brackets as delimiters for HTML tags, Markdown will treat them as +such. But if you write: + + 4 < 5 + +Markdown will translate it to: + + 4 < 5 + +However, inside Markdown code spans and blocks, angle brackets and +ampersands are *always* encoded automatically. This makes it easy to use +Markdown to write about HTML code. (As opposed to raw HTML, which is a +terrible format for writing about HTML syntax, because every single `<` +and `&` in your example code needs to be escaped.) + + +* * * + + +

    Block Elements

    + + +

    Paragraphs and Line Breaks

    + +A paragraph is simply one or more consecutive lines of text, separated +by one or more blank lines. (A blank line is any line that looks like a +blank line -- a line containing nothing but spaces or tabs is considered +blank.) Normal paragraphs should not be intended with spaces or tabs. + +The implication of the "one or more consecutive lines of text" rule is +that Markdown supports "hard-wrapped" text paragraphs. This differs +significantly from most other text-to-HTML formatters (including Movable +Type's "Convert Line Breaks" option) which translate every line break +character in a paragraph into a `
    ` tag. + +When you *do* want to insert a `
    ` break tag using Markdown, you +end a line with two or more spaces, then type return. + +Yes, this takes a tad more effort to create a `
    `, but a simplistic +"every line break is a `
    `" rule wouldn't work for Markdown. +Markdown's email-style [blockquoting][bq] and multi-paragraph [list items][l] +work best -- and look better -- when you format them with hard breaks. + + [bq]: #blockquote + [l]: #list + + + + + +Markdown supports two styles of headers, [Setext] [1] and [atx] [2]. + +Setext-style headers are "underlined" using equal signs (for first-level +headers) and dashes (for second-level headers). For example: + + This is an H1 + ============= + + This is an H2 + ------------- + +Any number of underlining `=`'s or `-`'s will work. + +Atx-style headers use 1-6 hash characters at the start of the line, +corresponding to header levels 1-6. For example: + + # This is an H1 + + ## This is an H2 + + ###### This is an H6 + +Optionally, you may "close" atx-style headers. This is purely +cosmetic -- you can use this if you think it looks better. The +closing hashes don't even need to match the number of hashes +used to open the header. (The number of opening hashes +determines the header level.) : + + # This is an H1 # + + ## This is an H2 ## + + ### This is an H3 ###### + + +

    Blockquotes

    + +Markdown uses email-style `>` characters for blockquoting. If you're +familiar with quoting passages of text in an email message, then you +know how to create a blockquote in Markdown. It looks best if you hard +wrap the text and put a `>` before every line: + + > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, + > consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. + > Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. + > + > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse + > id sem consectetuer libero luctus adipiscing. + +Markdown allows you to be lazy and only put the `>` before the first +line of a hard-wrapped paragraph: + + > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, + consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. + Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. + + > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse + id sem consectetuer libero luctus adipiscing. + +Blockquotes can be nested (i.e. a blockquote-in-a-blockquote) by +adding additional levels of `>`: + + > This is the first level of quoting. + > + > > This is nested blockquote. + > + > Back to the first level. + +Blockquotes can contain other Markdown elements, including headers, lists, +and code blocks: + + > ## This is a header. + > + > 1. This is the first list item. + > 2. This is the second list item. + > + > Here's some example code: + > + > return shell_exec("echo $input | $markdown_script"); + +Any decent text editor should make email-style quoting easy. For +example, with BBEdit, you can make a selection and choose Increase +Quote Level from the Text menu. + + +

    Lists

    + +Markdown supports ordered (numbered) and unordered (bulleted) lists. + +Unordered lists use asterisks, pluses, and hyphens -- interchangably +-- as list markers: + + * Red + * Green + * Blue + +is equivalent to: + + + Red + + Green + + Blue + +and: + + - Red + - Green + - Blue + +Ordered lists use numbers followed by periods: + + 1. Bird + 2. McHale + 3. Parish + +It's important to note that the actual numbers you use to mark the +list have no effect on the HTML output Markdown produces. The HTML +Markdown produces from the above list is: + +
      +
    1. Bird
    2. +
    3. McHale
    4. +
    5. Parish
    6. +
    + +If you instead wrote the list in Markdown like this: + + 1. Bird + 1. McHale + 1. Parish + +or even: + + 3. Bird + 1. McHale + 8. Parish + +you'd get the exact same HTML output. The point is, if you want to, +you can use ordinal numbers in your ordered Markdown lists, so that +the numbers in your source match the numbers in your published HTML. +But if you want to be lazy, you don't have to. + +If you do use lazy list numbering, however, you should still start the +list with the number 1. At some point in the future, Markdown may support +starting ordered lists at an arbitrary number. + +List markers typically start at the left margin, but may be indented by +up to three spaces. List markers must be followed by one or more spaces +or a tab. + +To make lists look nice, you can wrap items with hanging indents: + + * Lorem ipsum dolor sit amet, consectetuer adipiscing elit. + Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, + viverra nec, fringilla in, laoreet vitae, risus. + * Donec sit amet nisl. Aliquam semper ipsum sit amet velit. + Suspendisse id sem consectetuer libero luctus adipiscing. + +But if you want to be lazy, you don't have to: + + * Lorem ipsum dolor sit amet, consectetuer adipiscing elit. + Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, + viverra nec, fringilla in, laoreet vitae, risus. + * Donec sit amet nisl. Aliquam semper ipsum sit amet velit. + Suspendisse id sem consectetuer libero luctus adipiscing. + +If list items are separated by blank lines, Markdown will wrap the +items in `

    ` tags in the HTML output. For example, this input: + + * Bird + * Magic + +will turn into: + +

      +
    • Bird
    • +
    • Magic
    • +
    + +But this: + + * Bird + + * Magic + +will turn into: + +
      +
    • Bird

    • +
    • Magic

    • +
    + +List items may consist of multiple paragraphs. Each subsequent +paragraph in a list item must be intended by either 4 spaces +or one tab: + + 1. This is a list item with two paragraphs. Lorem ipsum dolor + sit amet, consectetuer adipiscing elit. Aliquam hendrerit + mi posuere lectus. + + Vestibulum enim wisi, viverra nec, fringilla in, laoreet + vitae, risus. Donec sit amet nisl. Aliquam semper ipsum + sit amet velit. + + 2. Suspendisse id sem consectetuer libero luctus adipiscing. + +It looks nice if you indent every line of the subsequent +paragraphs, but here again, Markdown will allow you to be +lazy: + + * This is a list item with two paragraphs. + + This is the second paragraph in the list item. You're + only required to indent the first line. Lorem ipsum dolor + sit amet, consectetuer adipiscing elit. + + * Another item in the same list. + +To put a blockquote within a list item, the blockquote's `>` +delimiters need to be indented: + + * A list item with a blockquote: + + > This is a blockquote + > inside a list item. + +To put a code block within a list item, the code block needs +to be indented *twice* -- 8 spaces or two tabs: + + * A list item with a code block: + + + + +It's worth noting that it's possible to trigger an ordered list by +accident, by writing something like this: + + 1986. What a great season. + +In other words, a *number-period-space* sequence at the beginning of a +line. To avoid this, you can backslash-escape the period: + + 1986\. What a great season. + + + +

    Code Blocks

    + +Pre-formatted code blocks are used for writing about programming or +markup source code. Rather than forming normal paragraphs, the lines +of a code block are interpreted literally. Markdown wraps a code block +in both `
    ` and `` tags.
    +
    +To produce a code block in Markdown, simply indent every line of the
    +block by at least 4 spaces or 1 tab. For example, given this input:
    +
    +    This is a normal paragraph:
    +
    +        This is a code block.
    +
    +Markdown will generate:
    +
    +    

    This is a normal paragraph:

    + +
    This is a code block.
    +    
    + +One level of indentation -- 4 spaces or 1 tab -- is removed from each +line of the code block. For example, this: + + Here is an example of AppleScript: + + tell application "Foo" + beep + end tell + +will turn into: + +

    Here is an example of AppleScript:

    + +
    tell application "Foo"
    +        beep
    +    end tell
    +    
    + +A code block continues until it reaches a line that is not indented +(or the end of the article). + +Within a code block, ampersands (`&`) and angle brackets (`<` and `>`) +are automatically converted into HTML entities. This makes it very +easy to include example HTML source code using Markdown -- just paste +it and indent it, and Markdown will handle the hassle of encoding the +ampersands and angle brackets. For example, this: + + + +will turn into: + +
    <div class="footer">
    +        &copy; 2004 Foo Corporation
    +    </div>
    +    
    + +Regular Markdown syntax is not processed within code blocks. E.g., +asterisks are just literal asterisks within a code block. This means +it's also easy to use Markdown to write about Markdown's own syntax. + + + +

    Horizontal Rules

    + +You can produce a horizontal rule tag (`
    `) by placing three or +more hyphens, asterisks, or underscores on a line by themselves. If you +wish, you may use spaces between the hyphens or asterisks. Each of the +following lines will produce a horizontal rule: + + * * * + + *** + + ***** + + - - - + + --------------------------------------- + + _ _ _ + + +* * * + +

    Span Elements

    + + + +Markdown supports two style of links: *inline* and *reference*. + +In both styles, the link text is delimited by [square brackets]. + +To create an inline link, use a set of regular parentheses immediately +after the link text's closing square bracket. Inside the parentheses, +put the URL where you want the link to point, along with an *optional* +title for the link, surrounded in quotes. For example: + + This is [an example](http://example.com/ "Title") inline link. + + [This link](http://example.net/) has no title attribute. + +Will produce: + +

    This is + an example inline link.

    + +

    This link has no + title attribute.

    + +If you're referring to a local resource on the same server, you can +use relative paths: + + See my [About](/about/) page for details. + +Reference-style links use a second set of square brackets, inside +which you place a label of your choosing to identify the link: + + This is [an example][id] reference-style link. + +You can optionally use a space to separate the sets of brackets: + + This is [an example] [id] reference-style link. + +Then, anywhere in the document, you define your link label like this, +on a line by itself: + + [id]: http://example.com/ "Optional Title Here" + +That is: + +* Square brackets containing the link identifier (optionally + indented from the left margin using up to three spaces); +* followed by a colon; +* followed by one or more spaces (or tabs); +* followed by the URL for the link; +* optionally followed by a title attribute for the link, enclosed + in double or single quotes. + +The link URL may, optionally, be surrounded by angle brackets: + + [id]: "Optional Title Here" + +You can put the title attribute on the next line and use extra spaces +or tabs for padding, which tends to look better with longer URLs: + + [id]: http://example.com/longish/path/to/resource/here + "Optional Title Here" + +Link definitions are only used for creating links during Markdown +processing, and are stripped from your document in the HTML output. + +Link definition names may constist of letters, numbers, spaces, and punctuation -- but they are *not* case sensitive. E.g. these two links: + + [link text][a] + [link text][A] + +are equivalent. + +The *implicit link name* shortcut allows you to omit the name of the +link, in which case the link text itself is used as the name. +Just use an empty set of square brackets -- e.g., to link the word +"Google" to the google.com web site, you could simply write: + + [Google][] + +And then define the link: + + [Google]: http://google.com/ + +Because link names may contain spaces, this shortcut even works for +multiple words in the link text: + + Visit [Daring Fireball][] for more information. + +And then define the link: + + [Daring Fireball]: http://daringfireball.net/ + +Link definitions can be placed anywhere in your Markdown document. I +tend to put them immediately after each paragraph in which they're +used, but if you want, you can put them all at the end of your +document, sort of like footnotes. + +Here's an example of reference links in action: + + I get 10 times more traffic from [Google] [1] than from + [Yahoo] [2] or [MSN] [3]. + + [1]: http://google.com/ "Google" + [2]: http://search.yahoo.com/ "Yahoo Search" + [3]: http://search.msn.com/ "MSN Search" + +Using the implicit link name shortcut, you could instead write: + + I get 10 times more traffic from [Google][] than from + [Yahoo][] or [MSN][]. + + [google]: http://google.com/ "Google" + [yahoo]: http://search.yahoo.com/ "Yahoo Search" + [msn]: http://search.msn.com/ "MSN Search" + +Both of the above examples will produce the following HTML output: + +

    I get 10 times more traffic from Google than from + Yahoo + or MSN.

    + +For comparison, here is the same paragraph written using +Markdown's inline link style: + + I get 10 times more traffic from [Google](http://google.com/ "Google") + than from [Yahoo](http://search.yahoo.com/ "Yahoo Search") or + [MSN](http://search.msn.com/ "MSN Search"). + +The point of reference-style links is not that they're easier to +write. The point is that with reference-style links, your document +source is vastly more readable. Compare the above examples: using +reference-style links, the paragraph itself is only 81 characters +long; with inline-style links, it's 176 characters; and as raw HTML, +it's 234 characters. In the raw HTML, there's more markup than there +is text. + +With Markdown's reference-style links, a source document much more +closely resembles the final output, as rendered in a browser. By +allowing you to move the markup-related metadata out of the paragraph, +you can add links without interrupting the narrative flow of your +prose. + + +

    Emphasis

    + +Markdown treats asterisks (`*`) and underscores (`_`) as indicators of +emphasis. Text wrapped with one `*` or `_` will be wrapped with an +HTML `` tag; double `*`'s or `_`'s will be wrapped with an HTML +`` tag. E.g., this input: + + *single asterisks* + + _single underscores_ + + **double asterisks** + + __double underscores__ + +will produce: + + single asterisks + + single underscores + + double asterisks + + double underscores + +You can use whichever style you prefer; the lone restriction is that +the same character must be used to open and close an emphasis span. + +Emphasis can be used in the middle of a word: + + un*fucking*believable + +But if you surround an `*` or `_` with spaces, it'll be treated as a +literal asterisk or underscore. + +To produce a literal asterisk or underscore at a position where it +would otherwise be used as an emphasis delimiter, you can backslash +escape it: + + \*this text is surrounded by literal asterisks\* + + + +

    Code

    + +To indicate a span of code, wrap it with backtick quotes (`` ` ``). +Unlike a pre-formatted code block, a code span indicates code within a +normal paragraph. For example: + + Use the `printf()` function. + +will produce: + +

    Use the printf() function.

    + +To include a literal backtick character within a code span, you can use +multiple backticks as the opening and closing delimiters: + + ``There is a literal backtick (`) here.`` + +which will produce this: + +

    There is a literal backtick (`) here.

    + +The backtick delimiters surrounding a code span may include spaces -- +one after the opening, one before the closing. This allows you to place +literal backtick characters at the beginning or end of a code span: + + A single backtick in a code span: `` ` `` + + A backtick-delimited string in a code span: `` `foo` `` + +will produce: + +

    A single backtick in a code span: `

    + +

    A backtick-delimited string in a code span: `foo`

    + +With a code span, ampersands and angle brackets are encoded as HTML +entities automatically, which makes it easy to include example HTML +tags. Markdown will turn this: + + Please don't use any `` tags. + +into: + +

    Please don't use any <blink> tags.

    + +You can write this: + + `—` is the decimal-encoded equivalent of `—`. + +to produce: + +

    &#8212; is the decimal-encoded + equivalent of &mdash;.

    + + + +

    Images

    + +Admittedly, it's fairly difficult to devise a "natural" syntax for +placing images into a plain text document format. + +Markdown uses an image syntax that is intended to resemble the syntax +for links, allowing for two styles: *inline* and *reference*. + +Inline image syntax looks like this: + + ![Alt text](/path/to/img.jpg) + + ![Alt text](/path/to/img.jpg "Optional title") + +That is: + +* An exclamation mark: `!`; +* followed by a set of square brackets, containing the `alt` + attribute text for the image; +* followed by a set of parentheses, containing the URL or path to + the image, and an optional `title` attribute enclosed in double + or single quotes. + +Reference-style image syntax looks like this: + + ![Alt text][id] + +Where "id" is the name of a defined image reference. Image references +are defined using syntax identical to link references: + + [id]: url/to/image "Optional title attribute" + +As of this writing, Markdown has no syntax for specifying the +dimensions of an image; if this is important to you, you can simply +use regular HTML `` tags. + + +* * * + + +

    Miscellaneous

    + + + +Markdown supports a shortcut style for creating "automatic" links for URLs and email addresses: simply surround the URL or email address with angle brackets. What this means is that if you want to show the actual text of a URL or email address, and also have it be a clickable link, you can do this: + + + +Markdown will turn this into: + + http://example.com/ + +Automatic links for email addresses work similarly, except that +Markdown will also perform a bit of randomized decimal and hex +entity-encoding to help obscure your address from address-harvesting +spambots. For example, Markdown will turn this: + + + +into something like this: + + address@exa + mple.com + +which will render in a browser as a clickable link to "address@example.com". + +(This sort of entity-encoding trick will indeed fool many, if not +most, address-harvesting bots, but it definitely won't fool all of +them. It's better than nothing, but an address published in this way +will probably eventually start receiving spam.) + + + +

    Backslash Escapes

    + +Markdown allows you to use backslash escapes to generate literal +characters which would otherwise have special meaning in Markdown's +formatting syntax. For example, if you wanted to surround a word with +literal asterisks (instead of an HTML `` tag), you can backslashes +before the asterisks, like this: + + \*literal asterisks\* + +Markdown provides backslash escapes for the following characters: + + \ backslash + ` backtick + * asterisk + _ underscore + {} curly braces + [] square brackets + () parentheses + # hash mark + + plus sign + - minus sign (hyphen) + . dot + ! exclamation mark + diff --git a/pkg/blackfriday/testdata/Nested blockquotes.html b/pkg/blackfriday/testdata/Nested blockquotes.html new file mode 100644 index 00000000..538bb4fe --- /dev/null +++ b/pkg/blackfriday/testdata/Nested blockquotes.html @@ -0,0 +1,9 @@ +
    +

    foo

    + +
    +

    bar

    +
    + +

    foo

    +
    diff --git a/pkg/blackfriday/testdata/Nested blockquotes.text b/pkg/blackfriday/testdata/Nested blockquotes.text new file mode 100644 index 00000000..ed3c624f --- /dev/null +++ b/pkg/blackfriday/testdata/Nested blockquotes.text @@ -0,0 +1,5 @@ +> foo +> +> > bar +> +> foo diff --git a/pkg/blackfriday/testdata/Ordered and unordered lists.html b/pkg/blackfriday/testdata/Ordered and unordered lists.html new file mode 100644 index 00000000..d6fa4278 --- /dev/null +++ b/pkg/blackfriday/testdata/Ordered and unordered lists.html @@ -0,0 +1,166 @@ +

    Unordered

    + +

    Asterisks tight:

    + +
      +
    • asterisk 1
    • +
    • asterisk 2
    • +
    • asterisk 3
    • +
    + +

    Asterisks loose:

    + +
      +
    • asterisk 1

    • + +
    • asterisk 2

    • + +
    • asterisk 3

    • +
    + +
    + +

    Pluses tight:

    + +
      +
    • Plus 1
    • +
    • Plus 2
    • +
    • Plus 3
    • +
    + +

    Pluses loose:

    + +
      +
    • Plus 1

    • + +
    • Plus 2

    • + +
    • Plus 3

    • +
    + +
    + +

    Minuses tight:

    + +
      +
    • Minus 1
    • +
    • Minus 2
    • +
    • Minus 3
    • +
    + +

    Minuses loose:

    + +
      +
    • Minus 1

    • + +
    • Minus 2

    • + +
    • Minus 3

    • +
    + +

    Ordered

    + +

    Tight:

    + +
      +
    1. First
    2. +
    3. Second
    4. +
    5. Third
    6. +
    + +

    and:

    + +
      +
    1. One
    2. +
    3. Two
    4. +
    5. Three
    6. +
    + +

    Loose using tabs:

    + +
      +
    1. First

    2. + +
    3. Second

    4. + +
    5. Third

    6. +
    + +

    and using spaces:

    + +
      +
    1. One

    2. + +
    3. Two

    4. + +
    5. Three

    6. +
    + +

    Multiple paragraphs:

    + +
      +
    1. Item 1, graf one.

      + +

      Item 2. graf two. The quick brown fox jumped over the lazy dog's +back.

    2. + +
    3. Item 2.

    4. + +
    5. Item 3.

    6. +
    + +

    Nested

    + +
      +
    • Tab + +
        +
      • Tab + +
          +
        • Tab
        • +
      • +
    • +
    + +

    Here's another:

    + +
      +
    1. First
    2. +
    3. Second: + +
        +
      • Fee
      • +
      • Fie
      • +
      • Foe
      • +
    4. +
    5. Third
    6. +
    + +

    Same thing but with paragraphs:

    + +
      +
    1. First

    2. + +
    3. Second:

      + +
        +
      • Fee
      • +
      • Fie
      • +
      • Foe
      • +
    4. + +
    5. Third

    6. +
    + +

    This was an error in Markdown 1.0.1:

    + +
      +
    • this

      + +
        +
      • sub
      • +
      + +

      that

    • +
    diff --git a/pkg/blackfriday/testdata/Ordered and unordered lists.text b/pkg/blackfriday/testdata/Ordered and unordered lists.text new file mode 100644 index 00000000..7f3b4977 --- /dev/null +++ b/pkg/blackfriday/testdata/Ordered and unordered lists.text @@ -0,0 +1,131 @@ +## Unordered + +Asterisks tight: + +* asterisk 1 +* asterisk 2 +* asterisk 3 + + +Asterisks loose: + +* asterisk 1 + +* asterisk 2 + +* asterisk 3 + +* * * + +Pluses tight: + ++ Plus 1 ++ Plus 2 ++ Plus 3 + + +Pluses loose: + ++ Plus 1 + ++ Plus 2 + ++ Plus 3 + +* * * + + +Minuses tight: + +- Minus 1 +- Minus 2 +- Minus 3 + + +Minuses loose: + +- Minus 1 + +- Minus 2 + +- Minus 3 + + +## Ordered + +Tight: + +1. First +2. Second +3. Third + +and: + +1. One +2. Two +3. Three + + +Loose using tabs: + +1. First + +2. Second + +3. Third + +and using spaces: + +1. One + +2. Two + +3. Three + +Multiple paragraphs: + +1. Item 1, graf one. + + Item 2. graf two. The quick brown fox jumped over the lazy dog's + back. + +2. Item 2. + +3. Item 3. + + + +## Nested + +* Tab + * Tab + * Tab + +Here's another: + +1. First +2. Second: + * Fee + * Fie + * Foe +3. Third + +Same thing but with paragraphs: + +1. First + +2. Second: + * Fee + * Fie + * Foe + +3. Third + + +This was an error in Markdown 1.0.1: + +* this + + * sub + + that diff --git a/pkg/blackfriday/testdata/Strong and em together.html b/pkg/blackfriday/testdata/Strong and em together.html new file mode 100644 index 00000000..71ec78c7 --- /dev/null +++ b/pkg/blackfriday/testdata/Strong and em together.html @@ -0,0 +1,7 @@ +

    This is strong and em.

    + +

    So is this word.

    + +

    This is strong and em.

    + +

    So is this word.

    diff --git a/pkg/blackfriday/testdata/Strong and em together.text b/pkg/blackfriday/testdata/Strong and em together.text new file mode 100644 index 00000000..95ee690d --- /dev/null +++ b/pkg/blackfriday/testdata/Strong and em together.text @@ -0,0 +1,7 @@ +***This is strong and em.*** + +So is ***this*** word. + +___This is strong and em.___ + +So is ___this___ word. diff --git a/pkg/blackfriday/testdata/Tabs.html b/pkg/blackfriday/testdata/Tabs.html new file mode 100644 index 00000000..509b41c6 --- /dev/null +++ b/pkg/blackfriday/testdata/Tabs.html @@ -0,0 +1,26 @@ +
      +
    • this is a list item +indented with tabs

    • + +
    • this is a list item +indented with spaces

    • +
    + +

    Code:

    + +
    this code block is indented by one tab
    +
    + +

    And:

    + +
    	this code block is indented by two tabs
    +
    + +

    And:

    + +
    +	this is an example list item
    +	indented with tabs
    +
    ++   this is an example list item
    +    indented with spaces
    +
    diff --git a/pkg/blackfriday/testdata/Tabs.text b/pkg/blackfriday/testdata/Tabs.text new file mode 100644 index 00000000..589d1136 --- /dev/null +++ b/pkg/blackfriday/testdata/Tabs.text @@ -0,0 +1,21 @@ ++ this is a list item + indented with tabs + ++ this is a list item + indented with spaces + +Code: + + this code block is indented by one tab + +And: + + this code block is indented by two tabs + +And: + + + this is an example list item + indented with tabs + + + this is an example list item + indented with spaces diff --git a/pkg/blackfriday/testdata/Tidyness.html b/pkg/blackfriday/testdata/Tidyness.html new file mode 100644 index 00000000..9c45b69c --- /dev/null +++ b/pkg/blackfriday/testdata/Tidyness.html @@ -0,0 +1,9 @@ +
    +

    A list within a blockquote:

    + +
      +
    • asterisk 1
    • +
    • asterisk 2
    • +
    • asterisk 3
    • +
    +
    diff --git a/pkg/blackfriday/testdata/Tidyness.text b/pkg/blackfriday/testdata/Tidyness.text new file mode 100644 index 00000000..5f18b8da --- /dev/null +++ b/pkg/blackfriday/testdata/Tidyness.text @@ -0,0 +1,5 @@ +> A list within a blockquote: +> +> * asterisk 1 +> * asterisk 2 +> * asterisk 3 diff --git a/prompts/prompts.go b/prompts/prompts.go new file mode 100644 index 00000000..abf9e242 --- /dev/null +++ b/prompts/prompts.go @@ -0,0 +1,63 @@ +package prompts + +import ( + "fmt" + "io/ioutil" + "strings" + "unicode" + + "github.com/smallstep/cli/errs" + "github.com/smallstep/cli/utils/reader" +) + +// Password prompts the user for a password if the provided pwdFilePath is +// empty, otherwise, it reads the password value from the given file path. +// +// If generate is true then a password will be generated for the user if the +// field is left empty. +func Password(prompt, name, pwdFilePath string, generate bool) (string, error) { + var value string + var err error + if pwdFilePath != "" { + value, err = readPasswordFile(pwdFilePath) + if err != nil { + return value, errs.Wrap(err, "Could not read password file '%s'", pwdFilePath) + } + + return value, nil + } + + valid := reader.RetryOnEmpty + if generate { + prompt = fmt.Sprintf("%s [leave blank to generate one automatically]: ", prompt) + valid = reader.GeneratePasswordOnEmpty + } else { + prompt = fmt.Sprintf("%s: ", prompt) + } + + err = reader.ReadPasswordSubtle(prompt, &value, name, valid) + if err != nil { + return "", err + } + + return value, nil +} + +func readPasswordFile(path string) (string, error) { + b, err := ioutil.ReadFile(path) + if err != nil { + return "", err + } + + return stripWhitespace(string(b[:])), nil +} + +func stripWhitespace(s string) string { + return strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return -1 + } + + return r + }, s) +} diff --git a/usage/renderer.go b/usage/renderer.go new file mode 100644 index 00000000..a0b39421 --- /dev/null +++ b/usage/renderer.go @@ -0,0 +1,355 @@ +package usage + +import ( + "bufio" + "bytes" + "fmt" + "io" + "regexp" + "strings" + "text/tabwriter" + + "github.com/samfoo/ansi" + md "github.com/smallstep/cli/pkg/blackfriday" +) + +// Render renders the given data with a custom markdown renderer. +func Render(b []byte) []byte { + return md.Run(b, md.WithRenderer(&Renderer{6, 0, nil, nil, false})) +} + +var colorEscapeRe = regexp.MustCompile(`\033\[\d*(;\d*)?m?\]?`) +var maxLineLength = 80 + +func stripColors(b []byte) []byte { + return colorEscapeRe.ReplaceAll(b, []byte("")) +} + +type item struct { + flags md.ListType + term []byte + definitions [][]byte +} + +type list struct { + items []item + flags md.ListType + parent *list +} + +func (l *list) isUnordered() bool { + return !l.isOrdered() && !l.isDefinition() +} + +func (l *list) isOrdered() bool { + return l.flags&md.ListTypeOrdered != 0 +} + +func (l *list) isDefinition() bool { + return l.flags&md.ListTypeDefinition != 0 +} + +func (l *list) containsBlock() bool { + // TODO: Not sure if we have to check every item or if it gets + // automatically set on the list? + return l.flags&md.ListItemContainsBlock != 0 +} + +type bufqueue struct { + w io.Writer + buf *bytes.Buffer + next *bufqueue + mode RenderMode +} + +// RenderMode enumerates different line breaks modes. +type RenderMode int + +const ( + // RenderModeKeepBreaks will keep the line breaks in the docs. + RenderModeKeepBreaks RenderMode = iota + // RenderModeBreakLines will automatically wrap the lines. + RenderModeBreakLines +) + +// Renderer implements a custom markdown renderer for blackfriday. +type Renderer struct { + depth int + listdepth int + list *list + out *bufqueue + inpara bool +} + +func (r *Renderer) write(b []byte) { + r.out.w.Write(b) +} + +func (r *Renderer) printf(s string, a ...interface{}) { + fmt.Fprintf(r.out.w, s, a...) +} + +func (r *Renderer) capture(mode RenderMode) { + buf := new(bytes.Buffer) + r.out = &bufqueue{buf, buf, r.out, mode} +} + +func (r *Renderer) finishCapture() *bytes.Buffer { + buf := r.out.buf + r.out = r.out.next + return buf +} + +func (r *Renderer) inParagraph() bool { + return r.inpara +} + +func (r *Renderer) inList() bool { + return r.list != nil +} + +func (r *Renderer) renderParagraphKeepBreaks(buf *bytes.Buffer) { + scanner := bufio.NewScanner(buf) + for scanner.Scan() { + r.printf(strings.Repeat(" ", r.depth)+"%s\n", scanner.Text()) + } +} + +func (r *Renderer) renderParagraphBreakLines(buf *bytes.Buffer, maxlen int) { + maxlen = maxlen - r.depth + scanner := bufio.NewScanner(buf) + scanner.Split(bufio.ScanWords) + line := []string{} + length := 0 + for scanner.Scan() { + word := scanner.Text() + wordLength := len(stripColors([]byte(word))) + // Print the line if we've got a collection of words over 80 characters, or if + // we have a single word that is over 80 characters on an otherwise empty line. + if length+wordLength > maxlen { + r.printf(strings.Repeat(" ", r.depth)+"%s\n", strings.Join(line, " ")) + line = []string{word} + length = wordLength + } else if length == 0 && wordLength > maxlen { + r.printf(strings.Repeat(" ", r.depth)+"%s\n", word) + } else { + line = append(line, word) + length += wordLength + 1 // Plus one for space + } + } + if len(line) > 0 { + r.printf(strings.Repeat(" ", r.depth)+"%s\n", strings.Join(line, " ")) + } +} + +func (r *Renderer) renderParagraph(buf *bytes.Buffer) { + switch r.out.mode { + case RenderModeKeepBreaks: + r.renderParagraphKeepBreaks(buf) + case RenderModeBreakLines: + r.renderParagraphBreakLines(buf, maxLineLength) + } +} + +// RenderNode implements blackfriday.Renderer interface. +func (r *Renderer) RenderNode(w io.Writer, node *md.Node, entering bool) md.WalkStatus { + if r.out == nil { + r.out = &bufqueue{w, nil, nil, RenderModeBreakLines} + } + + switch node.Type { + case md.Paragraph: + // Alternative idea here: call r.RenderNode() with our new buffer as + // `w`. In the `else` condition here render to the outter buffer and + // always return md.Terminate. So when we enter a paragraph we start + // parsing with a new output buffer and capture the output. + if entering { + if r.inParagraph() { + panic("already in paragraph") + } + r.inpara = true + //r.printf(out, "[paragraph:") + r.capture(r.out.mode) + } else { + r.renderParagraph(r.finishCapture()) + // Write a newline unless the parent node is a definition list term. + if node.Parent.Type != md.Item || node.Parent.ListFlags&md.ListTypeTerm == 0 { + r.printf("\n") + } + r.inpara = false + //r.printf(w, ":paragraph]") + } + case md.Text: + // TODO: is this necessary? I think all text is in a paragraph. + if r.inParagraph() { + r.write(node.Literal) + } else { + s := strings.Replace(string(node.Literal), "\n", "\n"+strings.Repeat(" ", r.depth), -1) + r.printf(s) + } + case md.Heading: + if entering { + r.printf(ansi.ColorCode("default+bh")) + } else { + r.printf(ansi.Reset) + r.printf("\n") + } + case md.Link: + if entering { + r.printf(ansi.ColorCode("default+b")) + //r.printf("\033[2m") // Dim + } else { + r.printf(ansi.Reset) + } + case md.Strong: + if entering { + r.printf(ansi.ColorCode("default+bh")) + } else { + r.printf(ansi.Reset) + } + case md.Emph: + if entering { + r.printf(ansi.ColorCode("default+u")) + } else { + r.printf(ansi.Reset) + } + case md.Code: + r.printf(ansi.ColorCode("default+u")) + r.write(node.Literal) + r.printf(ansi.Reset) + case md.List: + if entering { + r.listdepth++ + r.list = &list{[]item{}, node.ListFlags, r.list} + //r.printf("[list (type %s:", node.ListData.ListFlags) + } else { + if r.listdepth > 1 && r.list.isDefinition() { + w := new(tabwriter.Writer) + w.Init(r.out.w, 0, 8, 4, ' ', 0) + for _, item := range r.list.items { + fmt.Fprintf(w, strings.TrimRight(string(item.term), " \n")) + fmt.Fprintf(w, "\t") + for _, def := range item.definitions { + fmt.Fprintf(w, strings.Trim(string(def), " \n")) + } + fmt.Fprintf(w, "\t\n") + } + w.Flush() + r.printf("\n") + } else { + for _, item := range r.list.items { + r.write(item.term) + for _, def := range item.definitions { + r.write(def) + } + } + } + r.listdepth-- + r.list = r.list.parent + //r.printf(":list]") + } + case md.Item: + incdepth := 4 + //ltype := "normal" + if node.ListFlags&md.ListTypeTerm != 0 { + // Nested definition list terms get indented two spaces. Non-nested + // definition list terms are not indented. + if r.listdepth > 1 { + incdepth = 2 + } else { + incdepth = 0 + } + //ltype = "dt" + } else if node.ListFlags&md.ListTypeDefinition != 0 { + incdepth = 4 + //ltype = "dd" + } + + if entering { + //fmt.Fprintf(out, "[list item %s:", ltype) + r.depth += incdepth + if r.listdepth > 1 && r.list.isDefinition() { + r.capture(RenderModeKeepBreaks) + } else { + r.capture(RenderModeBreakLines) + } + if !r.list.isDefinition() || node.ListFlags&md.ListTypeTerm != 0 { + r.list.items = append(r.list.items, item{node.ListFlags, nil, nil}) + } + } else { + //fmt.Fprintf(out, ":list item]") + r.depth -= incdepth + buf := r.finishCapture() + if r.list.isDefinition() && node.ListFlags&md.ListTypeTerm == 0 { + i := len(r.list.items) - 1 + r.list.items[i].definitions = append(r.list.items[i].definitions, buf.Bytes()) + } else { + r.list.items[len(r.list.items)-1].term = buf.Bytes() + } + } + case md.Table: + if entering { + r.capture(RenderModeKeepBreaks) + w := new(tabwriter.Writer) + w.Init(r.out.w, 1, 8, 2, ' ', tabwriter.StripEscape|tabwriter.FilterHTML) + r.out.w = w + } else { + r.out.w.(*tabwriter.Writer).Flush() + buf := r.finishCapture() + r.renderParagraphKeepBreaks(buf) + r.printf("\n") + } + case md.TableBody: + // Do nothing. + case md.TableHead: + if entering { + r.capture(r.out.mode) + } else { + // Markdown doens't have a way to create a table without headers. + // We've opted to fix that here by not rendering headers at all if + // they're empty. + result := r.finishCapture().Bytes() + if strings.TrimSpace(string(stripColors(result))) != "" { + parts := strings.Split(strings.TrimRight(string(result), "\t\n"), "\t") + for i := 0; i < len(parts); i++ { + parts[i] = "\xff" + ansi.ColorCode("default+bh") + "\xff" + parts[i] + "\xff" + ansi.Reset + "\xff" + } + r.printf(strings.Join(parts, "\t") + "\t\n") + } + } + case md.TableRow: + if entering { + r.capture(r.out.mode) + } else { + // Escape any colors in the row before writing to the + // tabwriter, otherwise they screw up the width calculations. The + // escape character for tabwriter is \xff. + result := r.finishCapture().Bytes() + result = colorEscapeRe.ReplaceAll(result, []byte("\xff$0\xff")) + r.write(result) + r.printf("\n") + } + case md.TableCell: + if !entering { + r.printf("\t") + } + case md.CodeBlock: + r.depth += 4 + r.renderParagraphKeepBreaks(bytes.NewBuffer(node.Literal)) + r.printf("\n") + r.depth -= 4 + case md.Document: + default: + r.printf("unknown block %s:", node.Type) + r.write(node.Literal) + } + //w.Write([]byte(fmt.Sprintf("node<%s; %t>", node.Type, entering))) + //w.Write(node.Literal) + return md.GoToNext +} + +// RenderHeader implements blackfriday.Renderer interface. +func (r *Renderer) RenderHeader(w io.Writer, ast *md.Node) {} + +// RenderFooter implements blackfriday.Renderer interface. +func (r *Renderer) RenderFooter(w io.Writer, ast *md.Node) {} diff --git a/usage/usage.go b/usage/usage.go new file mode 100644 index 00000000..2ef5c7c5 --- /dev/null +++ b/usage/usage.go @@ -0,0 +1,217 @@ +package usage + +import ( + "bytes" + "fmt" + "strings" + "text/template" +) + +var usageTextTempl = " {{.Name}}\n {{.Usage}} {{if .Required}}(Required){{else}}(Optional){{end}}{{if .Multiple}} (Multiple can be specified){{end}}\n" +var templ *template.Template + +func init() { + templ = template.Must(template.New("usageText").Parse(usageTextTempl)) +} + +// Argument specifies the Name, Usage, and whether or not an Argument is +// required or not +type Argument struct { + Required bool + Multiple bool + Name string + Usage string +} + +// Decorate returns the name of an Argument and decorates it with notation to +// indicate whether its required or not +func (a Argument) Decorate() string { + name := a.Name + if a.Multiple { + name = name + "(s)..." + } + if a.Required { + return fmt.Sprintf("<%s>", name) + } + + return fmt.Sprintf("[%s]", name) +} + +// Arguments is an array of Argument structs that specify which arguments are +// accepted by a Command +type Arguments []Argument + +// UsageText returns the value of the UsageText property for a cli.Command for +// these arguments +func (args Arguments) UsageText() string { + var buf bytes.Buffer + for _, a := range args { + data := map[string]interface{}{ + "Name": a.Decorate(), + "Multiple": a.Multiple, + "Required": a.Required, + "Usage": a.Usage, + } + + err := templ.Execute(&buf, data) + if err != nil { + panic(fmt.Sprintf("Could not generate args template for %s: %s", a.Name, err)) + } + } + + return "\n\n" + buf.String() +} + +// ArgsUsage returns the value of the ArgsUsage property for a cli.Command for +// these arguments +func (args Arguments) ArgsUsage() string { + out := "" + for i, a := range args { + out += a.Decorate() + if i < len(args)-1 { + out += " " + } + } + + return out +} + +// AppHelpTemplate contains the modified template for the main app +var AppHelpTemplate = `## NAME +**{{.HelpName}}** -- {{.Usage}} + +## USAGE +{{if .UsageText}}{{.UsageText}}{{else}}**{{.HelpName}}**{{if .Commands}} {{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}_[arguments]_{{end}}{{end}}{{if .Description}} + +## STABILITY INDEX + +FOO BAR BAZ + +## DESCRIPTION +{{.Description}}{{end}}{{if .VisibleCommands}} + +## COMMANDS + +{{range .VisibleCategories}}{{if .Name}}{{.Name}}:{{end}} +||| +|---|---|{{range .VisibleCommands}} +| **{{join .Names ", "}}** | {{.Usage}} |{{end}} +{{end}}{{if .VisibleFlags}}{{end}} + +## OPTIONS + +{{range $index, $option := .VisibleFlags}}{{if $index}} +{{end}}{{$option}} +{{end}}{{end}}{{if .Copyright}}{{if len .Authors}} + +## AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: + +{{range $index, $author := .Authors}}{{if $index}} +{{end}}{{$author}}{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} + +## ONLINE + +This documentation is available online at https://smallstep.com/documentation + +## PRINTING + +This documentation can be typeset for printing by running ... + +A version of this document typeset for printing is available online at ...pdf + +## VERSION + +{{.Version}}{{end}}{{end}} + +## COPYRIGHT + +{{.Copyright}} +{{end}} +` + +// SubcommandHelpTemplate contains the modified template for a sub command +// Note that the weird "|||\n|---|---|" syntax sets up a markdown table with empty headers. +var SubcommandHelpTemplate = `## NAME +**{{.HelpName}}** -- {{.Usage}} + +## USAGE + +{{if .UsageText}}{{.UsageText}}{{else}}**{{.HelpName}}** {{if .VisibleFlags}} _[options]_{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}_[arguments]_{{end}}{{end}}{{if .Description}} + +## DESCRIPTION + +{{.Description}}{{end}} + +## COMMANDS + +{{range .VisibleCategories}}{{if .Name}}{{.Name}}:{{end}} +||| +|---|---|{{range .VisibleCommands}} +| **{{join .Names ", "}}** | {{.Usage}} |{{end}} +{{end}}{{if .VisibleFlags}} + +## OPTIONS + +{{range .VisibleFlags}} +{{.}} +{{end}}{{end}} + +## SUBCOMMAND TEMPLATE +` + +// CommandHelpTemplate contains the modified template for a command +var CommandHelpTemplate = `## NAME +**{{.HelpName}}** -- {{.Usage}} + +## USAGE + +{{if .UsageText}}{{.UsageText}}{{else}}**{{.HelpName}}**{{if .VisibleFlags}} _[options]_{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}_[arguments]_{{end}}{{end}}{{if .Category}} + +## CATEGORY + +{{.Category}}{{end}}{{if .Description}} + +## DESCRIPTION + +{{.Description}}{{end}}{{if .VisibleFlags}} + +## OPTIONS + +{{range .VisibleFlags}} +{{.}} +{{end}}{{end}} + +## COMMAND TEMPLATE +` + +// FlagNamePrefixer converts a full flag name and its placeholder into the help +// message flag prefix. This is used by the default FlagStringer. +// +// This method clones urflave/cli functionality but adds a new line at the end. +func FlagNamePrefixer(fullName, placeholder string) string { + var prefixed string + parts := strings.Split(fullName, ",") + for i, name := range parts { + name = strings.Trim(name, " ") + prefixed += "**" + prefixFor(name) + name + "**" + + if placeholder != "" { + prefixed += "=" + placeholder + } + if i < len(parts)-1 { + prefixed += ", " + } + } + //return "* " + prefixed + "\n" + return prefixed + "\n: " +} + +func prefixFor(name string) (prefix string) { + if len(name) == 1 { + prefix = "-" + } else { + prefix = "--" + } + + return +} diff --git a/utils/reader/read.go b/utils/reader/read.go new file mode 100644 index 00000000..2be62223 --- /dev/null +++ b/utils/reader/read.go @@ -0,0 +1,158 @@ +package reader + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + "syscall" + + "github.com/pkg/errors" + "github.com/smallstep/cli/crypto" + "golang.org/x/crypto/ssh/terminal" +) + +var passwordLength = 64 + +type retryError struct{} + +func (re *retryError) Error() string { + return "Need to retry" +} + +// DefaultOnEmpty replaces pointer value with default value if the value is an empty string. +func DefaultOnEmpty(def string) func(*string, string) error { + return func(ptr *string, key string) error { + if ptr == nil { + return errors.Errorf("pointer cannot be nil") + } + + if len(*ptr) == 0 { + *ptr = def + } + return nil + } +} + +// CurrentDirectoryOnEmpty replaces pointer value with current directory +// if the value is an empty string. +func CurrentDirectoryOnEmpty(ptr *string, key string) error { + if ptr == nil { + return errors.Errorf("pointer cannot be nil") + } + + if len(*ptr) == 0 { + *ptr = "." + } + return nil +} + +// FailOnEmpty returns an error if the pointer value is an empty string. +func FailOnEmpty(ptr *string, key string) error { + if ptr == nil { + return errors.Errorf("pointer cannot be nil") + } + if len(*ptr) == 0 { + return errors.Errorf("%s parameter cannot be empty", key) + } + return nil +} + +// GeneratePasswordOnEmpty replaces pointer value with a newly generated +// password if the value is an empty string. +func GeneratePasswordOnEmpty(ptr *string, key string) error { + if ptr == nil { + return errors.Errorf("pointer cannot be nil") + } + + if len(*ptr) == 0 { + var err error + if *ptr, err = crypto.GenerateRandomRestrictedString(passwordLength); err != nil { + return errors.Wrapf(err, "Failed to generate %s", key) + } + fmt.Printf("\n\n%s: %s\n\n", key, *ptr) + return nil + } + fmt.Println() + return nil +} + +// RetryOnEmpty returns a retryError if the value in the ptr is an empty string. +func RetryOnEmpty(ptr *string, key string) error { + if ptr == nil { + return errors.Errorf("pointer cannot be nil") + } + + if len(*ptr) == 0 { + return &retryError{} + } + return nil +} + +type valid func(*string, string) error + +// ReadPassword reads password into ptr from stdin - input silenced. +func ReadPassword(prompt string, ptr *string, key string, v valid) error { + return _readPassword(prompt, os.Stdout, ptr, key, v) +} + +// ReadPasswordSubtle reads password from stdin, but prompt goes to stderr +// rather than stdout. +func ReadPasswordSubtle(prompt string, ptr *string, key string, v valid) error { + return _readPassword(prompt, os.Stderr, ptr, key, v) +} + +func _readPassword(prompt string, w io.Writer, ptr *string, key string, v valid) error { + if ptr == nil { + return errors.Errorf("pointer cannot be nil") + } + + fmt.Fprintf(w, prompt) + temp, err := terminal.ReadPassword(int(syscall.Stdin)) + // Need to add newline, b/c it will be swallowed by ReadPassword. + if w == os.Stderr { + fmt.Fprintf(w, "\n") + } + if err != nil { + return errors.WithStack(err) + } + *ptr = strings.TrimSpace(string(temp)) + + if err = v(ptr, key); err == nil { + return nil + } + switch err.(type) { + case *retryError: + return _readPassword(prompt, w, ptr, key, v) + default: + return errors.WithStack(err) + } +} + +// ReadString reads string into ptr from stdin. +func ReadString(reader *bufio.Reader, prompt string, ptr *string, key string, v valid) error { + if reader == nil { + return errors.Errorf("reader cannot be nil") + } + if ptr == nil { + return errors.Errorf("pointer cannot be nil") + } + + fmt.Printf(prompt) + temp, err := reader.ReadString('\n') + if err != nil { + return errors.WithStack(err) + } + *ptr = strings.TrimSpace(temp) + + if err = v(ptr, key); err == nil { + return nil + } + switch err.(type) { + case *retryError: + return ReadString(reader, prompt, ptr, key, v) + default: + return errors.WithStack(err) + } +} diff --git a/utils/reader/read_test.go b/utils/reader/read_test.go new file mode 100644 index 00000000..c71abced --- /dev/null +++ b/utils/reader/read_test.go @@ -0,0 +1,368 @@ +package reader + +import ( + "bufio" + "fmt" + "io" + "strings" + "testing" + + "github.com/bouk/monkey" + "github.com/pkg/errors" + "golang.org/x/crypto/ssh/terminal" +) + +func Test_CurrentDirectoryOnEmpty(t *testing.T) { + // nil pointer + var ptr *string + if err := CurrentDirectoryOnEmpty(ptr, ""); err == nil { + t.Errorf("expected: `error`, but got: `nil`") + } else { + expected := "pointer cannot be nil" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch: expected: `%s`, but got: `%s`", + expected, err.Error()) + } + } + + // empty string + temp := "" + ptr = &temp + if err := CurrentDirectoryOnEmpty(ptr, ""); err == nil { + if ptr == nil { + t.Errorf("should not be nil") + } else if strings.Compare(*ptr, ".") != 0 { + t.Errorf("data mismatch -- expected: `%s`, but got: `%s`", + ".", *ptr) + } + } else { + t.Errorf("CurrentDirectoryOnEmpty error: %s", err) + } + + // populated string + temp = "shake and bake" + ptr = &temp + if err := CurrentDirectoryOnEmpty(ptr, ""); err == nil { + if ptr == nil { + t.Errorf("should not be nil") + } else if strings.Compare(*ptr, temp) != 0 { + t.Errorf("data mismatch -- expected: `%s`, but got: `%s`", + temp, *ptr) + } + } else { + t.Errorf("CurrentDirectoryOnEmpty error: %s", err) + } +} + +func Test_FailOnEmpty(t *testing.T) { + key := "Country" + + // nil pointer + if err := FailOnEmpty(nil, key); err == nil { + t.Errorf("expected: `error`, but got: `nil`") + } else { + expected := "pointer cannot be nil" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch: expected: `%s`, but got: `%s`", + expected, err.Error()) + } + } + + // failure on empty ptr string value + empty := "" + ptr := &empty + if err := FailOnEmpty(ptr, key); err == nil { + t.Errorf("expected: `error`, but got: `nil`") + } else { + expected := fmt.Sprintf("%s parameter cannot be empty", key) + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch: expected: `%s`, but got: `%s`", + expected, err.Error()) + } + } + + // no change unpopulated string + ptr = &key + if err := FailOnEmpty(ptr, key); err == nil { + if *ptr != key { + t.Errorf("error mismatch: expected: `%s`, but got: `%s`", + key, *ptr) + } + } else { + t.Errorf("FailOnEmpty error: %s", err) + } +} + +func Test_GeneratePasswordOnEmpty(t *testing.T) { + // nil pointer + var ptr *string + if err := GeneratePasswordOnEmpty(ptr, ""); err == nil { + t.Errorf("expected: `error`, but got: `nil`") + } else { + expected := "pointer cannot be nil" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch: expected: `%s`, but got: `%s`", + expected, err.Error()) + } + } + + // empty string + temp := "" + ptr = &temp + if err := GeneratePasswordOnEmpty(ptr, ""); err == nil { + if ptr == nil { + t.Errorf("should not be nil") + } else if len(*ptr) != passwordLength { + t.Errorf("password length mismatch -- expected: `%d`, but got: `%d`", + passwordLength, len(*ptr)) + } + } else { + t.Errorf("CurrentDirectoryOnEmpty error: %s", err) + } + + // populated string + temp = "shake and bake" + ptr = &temp + if err := GeneratePasswordOnEmpty(ptr, "password"); err == nil { + if *ptr != temp { + t.Errorf("data mismatch -- expected: `%s`, but got: `%s`", + temp, *ptr) + } + } else { + t.Errorf("GeneratePasswordOnEmpty error: %s", err) + } +} + +type Reader struct { + read string + done bool +} + +func NewReader(toRead string) *Reader { + return &Reader{toRead, false} +} + +func (r *Reader) Read(p []byte) (n int, err error) { + if r.done { + return 0, io.EOF + } + for i, b := range []byte(r.read) { + p[i] = b + } + r.done = true + return len(r.read), nil +} + +type ErrorReader struct { + read string + done bool +} + +func NewErrorReader(toRead string) *ErrorReader { + return &ErrorReader{toRead, false} +} + +func (r *ErrorReader) Read(p []byte) (n int, err error) { + return 0, errors.Errorf("bad read") +} + +func Test_ReadString(t *testing.T) { + // nil reader + if err := ReadString(nil, "", nil, "", CurrentDirectoryOnEmpty); err == nil { + t.Errorf("expected: `error`, but got: `nil`") + } else { + expected := "reader cannot be nil" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch: expected: `%s`, but got: `%s`", + expected, err.Error()) + } + } + + // nil ptr + reader := bufio.NewReader(NewReader("current\n")) + if err := ReadString(reader, "", nil, "", CurrentDirectoryOnEmpty); err == nil { + t.Errorf("expected: `error`, but got: `nil`") + } else { + expected := "pointer cannot be nil" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch: expected: `%s`, but got: `%s`", + expected, err.Error()) + } + } + + // onEmpty error + reader = bufio.NewReader(NewReader("\n")) + throws := func(arg1 *string, arg2 string) error { + return errors.Errorf("big-time error") + } + temp := "" + ptr := &temp + if err := ReadString(reader, "", ptr, "", throws); err == nil { + t.Errorf("expected: `error`, but got: `nil`") + } else { + expected := "big-time error" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch: expected: `%s`, but got: `%s`", + expected, err.Error()) + } + } + + // reader error + reader = bufio.NewReader(NewErrorReader("\n")) + temp = "" + ptr = &temp + if err := ReadString(reader, "", ptr, "", throws); err == nil { + t.Errorf("expected: `error`, but got: `nil`") + } else { + expected := "bad read" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch: expected: `%s`, but got: `%s`", + expected, err.Error()) + } + } + + // input nil gets replaced + reader = bufio.NewReader(NewReader("\n")) + temp = "" + ptr = &temp + if err := ReadString(reader, "", ptr, "", CurrentDirectoryOnEmpty); err == nil { + expected := "." + if strings.Compare(*ptr, expected) != 0 { + t.Errorf("data mismatch -- expected: `%s`, but got: `%s`", + expected, *ptr) + } + } else { + t.Errorf("ReadString error: %s", err) + } + + // non empty input is kept + reader = bufio.NewReader(NewReader("shake and bake\n")) + temp = "" + ptr = &temp + if err := ReadString(reader, "", ptr, "", CurrentDirectoryOnEmpty); err == nil { + expected := "shake and bake" + if strings.Compare(*ptr, expected) != 0 { + t.Errorf("data mismatch -- expected: `%s`, but got: `%s`", + expected, *ptr) + } + } else { + t.Errorf("ReadString error: %s", err) + } +} + +func Test_ReadString_retryOnEmpty(t *testing.T) { + i := true + f := ReadString + temp := "" + ptr := &temp + reader := bufio.NewReader(NewReader("\n")) + monkey.Patch(ReadString, func(r *bufio.Reader, p string, ptr *string, key string, v valid) error { + if i { + i = false + return f(r, p, ptr, key, v) + } + temp = "hi there" + return nil + }) + + if err := ReadString(reader, "", ptr, "common", RetryOnEmpty); err == nil { + if i || temp != "hi there" { + t.Errorf("failed to retry") + } + } + monkey.Unpatch(ReadString) +} + +func Test_ReadPassword_nilPtr(t *testing.T) { + // nil ptr + monkey.Patch(terminal.ReadPassword, func(i int) ([]uint8, error) { + return ([]uint8)("current"), nil + }) + if err := ReadPassword("", nil, "", FailOnEmpty); err == nil { + t.Errorf("expected: `error`, but got: `nil`") + } else { + expected := "pointer cannot be nil" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch: expected: `%s`, but got: `%s`", + expected, err.Error()) + } + } + monkey.Unpatch(terminal.ReadPassword) +} + +func Test_ReadPassword_onEmptyError(t *testing.T) { + monkey.Patch(terminal.ReadPassword, func(i int) ([]uint8, error) { + return ([]uint8)(""), nil + }) + if err := ReadPassword("", nil, "", FailOnEmpty); err == nil { + t.Errorf("expected: `error`, but got: `nil`") + } else { + expected := "pointer cannot be nil" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch: expected: `%s`, but got: `%s`", + expected, err.Error()) + } + } + monkey.Unpatch(terminal.ReadPassword) +} + +func Test_ReadPassword_spacesIsStillEmpty(t *testing.T) { + monkey.Patch(terminal.ReadPassword, func(i int) ([]uint8, error) { + return ([]uint8)(" "), nil + }) + if err := ReadPassword("", nil, "", FailOnEmpty); err == nil { + t.Errorf("expected: `error`, but got: `nil`") + } else { + expected := "pointer cannot be nil" + if !strings.HasPrefix(err.Error(), expected) { + t.Errorf("error mismatch: expected: `%s`, but got: `%s`", + expected, err.Error()) + } + } + monkey.Unpatch(terminal.ReadPassword) +} + +func Test_ReadPassword_nilPtrOverwrittenByInput(t *testing.T) { + // also tests trim space + monkey.Patch(terminal.ReadPassword, func(i int) ([]uint8, error) { + return ([]uint8)("shake and bake"), nil + }) + temp := "" + ptr := &temp + if err := ReadPassword("", ptr, "common", FailOnEmpty); err == nil { + expected := "shake and bake" + if strings.Compare(*ptr, expected) != 0 { + t.Errorf("data mismatch -- expected: `%s`, but got: `%s`", + expected, *ptr) + } + } else { + t.Errorf("ReadString error: %s", err) + } + monkey.Unpatch(terminal.ReadPassword) +} + +func Test_ReadPassword_retryOnEmpty(t *testing.T) { + i := true + f := ReadPassword + // also tests trim space + monkey.Patch(terminal.ReadPassword, func(i int) ([]uint8, error) { + return ([]uint8)(""), nil + }) + monkey.Patch(ReadPassword, func(p string, ptr *string, key string, v valid) error { + if i { + i = false + return f(p, ptr, key, v) + } + return nil + }) + + temp := "" + ptr := &temp + if err := ReadPassword("", ptr, "common", RetryOnEmpty); err == nil { + if i { + t.Errorf("failed to retry") + } + } + monkey.Unpatch(terminal.ReadPassword) + monkey.Unpatch(ReadPassword) +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 00000000..3ce68e98 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,19 @@ +package utils + +import ( + "fmt" + "os" +) + +// Fail prints out the error struct if STEPDEBUG is true otherwise it just +// prints out the error message. Finally, it exits with an error code of 1. +func Fail(err error) { + if err != nil { + if os.Getenv("STEPDEBUG") == "1" { + fmt.Fprintf(os.Stderr, "%+v\n", err) + } else { + fmt.Fprintln(os.Stderr, err) + } + os.Exit(1) + } +}