diff --git a/cmd/sshrimp-ca/main.go b/cmd/sshrimp-ca/main.go index 831ced6..6c58bbd 100644 --- a/cmd/sshrimp-ca/main.go +++ b/cmd/sshrimp-ca/main.go @@ -3,18 +3,11 @@ package main import ( "context" "crypto/rand" - "encoding/hex" - "errors" "fmt" - "math" - "math/big" - "regexp" - "time" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-lambda-go/lambdacontext" "github.com/stoggi/sshrimp/internal/config" - "github.com/stoggi/sshrimp/internal/identity" "github.com/stoggi/sshrimp/internal/signer" "golang.org/x/crypto/ssh" ) @@ -34,101 +27,14 @@ func HandleRequest(ctx context.Context, event signer.SSHrimpEvent) (*signer.SSHr return nil, err } - // Validate the user supplied public key - publicKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(event.PublicKey)) - if err != nil { - return nil, fmt.Errorf("unable to parse public key: %v", err) - } - - // Validate the user supplied identity token with the loaded configuration - i, err := identity.NewIdentity(c) - username, err := i.Validate(event.Token) + // Create the certificate struct with all our configured values + certificate, err := signer.ValidateRequest(event, c, lambdaContext.AwsRequestID, lambdaContext.InvokedFunctionArn) if err != nil { return nil, err } - // Validate and add force commands or source address options - criticalOptions := make(map[string]string) - if regexp.MustCompile(c.CertificateAuthority.ForceCommandRegex).MatchString(event.ForceCommand) { - if event.ForceCommand != "" { - criticalOptions["force-command"] = event.ForceCommand - } - } else { - return nil, errors.New("forcecommand validation failed") - } - if regexp.MustCompile(c.CertificateAuthority.SourceAddressRegex).MatchString(event.SourceAddress) { - if event.SourceAddress != "" { - criticalOptions["source-address"] = event.SourceAddress - } - } else { - return nil, errors.New("sourceaddress validation failed") - } - - // Generate a random nonce for the certificate - bytes := make([]byte, 32) - nonce := make([]byte, len(bytes)*2) - if _, err := rand.Read(bytes); err != nil { - return nil, err - } - hex.Encode(nonce, bytes) - - // Generate a random serial number - serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) - if err != nil { - return nil, err - } - - // Validate and set the certificate valid and expire times - now := time.Now() - validAfterOffset, err := time.ParseDuration(c.CertificateAuthority.ValidAfterOffset) - if err != nil { - return nil, err - } - validBeforeOffset, err := time.ParseDuration(c.CertificateAuthority.ValidBeforeOffset) - if err != nil { - return nil, err - } - validAfter := now.Add(validAfterOffset) - validBefore := now.Add(validBeforeOffset) - - // Convert the extensions slice to a map - extensions := make(map[string]string, len(c.CertificateAuthority.Extensions)) - for _, extension := range c.CertificateAuthority.Extensions { - extensions[extension] = "" - } - - // Create a key ID to be added to the certificate. Follows BLESS Key ID format - // https://github.com/Netflix/bless - keyID := fmt.Sprintf("request[%s] for[%s] from[%s] command[%s] ssh_key[%s] ca[%s] valid_to[%s]", - lambdaContext.AwsRequestID, - username, - event.SourceAddress, - event.ForceCommand, - ssh.FingerprintSHA256(publicKey), - lambdaContext.InvokedFunctionArn, - validBefore.Format("2006/01/02 15:04:05"), - ) - - // Create the certificate struct with all our configured alues - certificate := ssh.Certificate{ - Nonce: nonce, - Key: publicKey, - Serial: serial.Uint64(), - CertType: ssh.UserCert, - KeyId: keyID, - ValidPrincipals: []string{ - username, - }, - Permissions: ssh.Permissions{ - CriticalOptions: criticalOptions, - Extensions: extensions, - }, - ValidAfter: uint64(validAfter.Unix()), - ValidBefore: uint64(validBefore.Unix()), - } - // Setup our Certificate Authority signer backed by KMS - kmsSigner := signer.NewKMSSigner(c.CertificateAuthority.KeyAlias) + kmsSigner := signer.NewAWSSigner(c.CertificateAuthority.KeyAlias) sshAlgorithmSigner, err := signer.NewAlgorithmSignerFromSigner(kmsSigner, ssh.SigAlgoRSASHA2256) if err != nil { return nil, err diff --git a/gcp/gcp.go b/gcp/gcp.go new file mode 100644 index 0000000..f8141b7 --- /dev/null +++ b/gcp/gcp.go @@ -0,0 +1,77 @@ +package gcp + +import ( + "crypto/rand" + "encoding/json" + "fmt" + "net/http" + "os" + + "github.com/stoggi/sshrimp/internal/config" + "github.com/stoggi/sshrimp/internal/signer" + "golang.org/x/crypto/ssh" +) + +func httpError(w http.ResponseWriter, v interface{}, statusCode int) { + e := json.NewEncoder(w) + err := e.Encode(v) + http.Error(w, err.Error(), statusCode) +} + +// HandleRequest handles a request to sign an SSH public key verified by an OpenIDConnect id_token +func SSHrimp(w http.ResponseWriter, r *http.Request) { + + // Load the configuration file, if not exsits, exit. + c := config.NewSSHrimp() + if err := c.Read(config.GetPath()); err != nil { + httpError(w, signer.SSHrimpResult{"", err.Error(), http.StatusText(http.StatusInternalServerError)}, http.StatusInternalServerError) + return + } + + var event signer.SSHrimpEvent + if err := json.NewDecoder(r.Body).Decode(&event); err != nil { + httpError(w, signer.SSHrimpResult{"", err.Error(), http.StatusText(http.StatusBadRequest)}, http.StatusBadRequest) + return + } + + certificate, err := signer.ValidateRequest(event, c, r.Header.Get("Function-Execution-Id"), fmt.Sprintf("%s/%s/%s", os.Getenv("GCP_PROJECT"), os.Getenv("FUNCTION_REGION"), os.Getenv("FUNCTION_NAME"))) + if err != nil { + httpError(w, signer.SSHrimpResult{"", err.Error(), http.StatusText(http.StatusBadRequest)}, http.StatusBadRequest) + return + } + + // Setup our Certificate Authority signer backed by KMS + kmsSigner := signer.NewGCPSSigner(c.CertificateAuthority.KeyAlias) + + sshAlgorithmSigner, err := signer.NewAlgorithmSignerFromSigner(kmsSigner, ssh.SigAlgoRSASHA2256) + if err != nil { + httpError(w, signer.SSHrimpResult{"", err.Error(), http.StatusText(http.StatusBadRequest)}, http.StatusBadRequest) + return + } + + // Sign the certificate!! + if err := certificate.SignCert(rand.Reader, sshAlgorithmSigner); err != nil { + httpError(w, signer.SSHrimpResult{"", err.Error(), http.StatusText(http.StatusBadRequest)}, http.StatusBadRequest) + return + } + + // Extract the public key (certificate) to return to the user + pubkey, err := ssh.ParsePublicKey(certificate.Marshal()) + if err != nil { + httpError(w, signer.SSHrimpResult{"", err.Error(), http.StatusText(http.StatusBadRequest)}, http.StatusBadRequest) + return + } + + // Success! + res := &signer.SSHrimpResult{ + Certificate: string(ssh.MarshalAuthorizedKey(pubkey)), + ErrorMessage: "", + ErrorType: "", + } + e := json.NewEncoder(w) + err = e.Encode(res) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } +} diff --git a/go.mod b/go.mod index dfbee2b..83a5c63 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.13 replace github.com/b-b3rn4rd/gocfn => github.com/stoggi/gocfn v0.0.0-20200214083946-6202cea979b9 require ( + cloud.google.com/go v0.38.0 github.com/AlecAivazis/survey/v2 v2.0.5 github.com/BurntSushi/toml v0.3.1 github.com/alecthomas/colour v0.1.0 // indirect @@ -17,6 +18,7 @@ require ( github.com/coreos/go-oidc v2.1.0+incompatible github.com/ghodss/yaml v1.0.0 // indirect github.com/google/uuid v1.1.1 + github.com/googleapis/gax-go v2.0.2+incompatible // indirect github.com/hashicorp/aws-sdk-go-base v0.4.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/magefile/mage v1.9.0 @@ -28,6 +30,9 @@ require ( golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect + google.golang.org/api v0.21.0 // indirect + google.golang.org/genproto v0.0.0-20200413115906-b5235f65be36 + google.golang.org/grpc v1.28.1 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/square/go-jose.v2 v2.4.1 // indirect ) diff --git a/go.sum b/go.sum index 339f4a6..d52d49d 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,8 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= github.com/99designs/aws-vault v4.5.1+incompatible/go.mod h1:BKt7gBiUkiAOh7TP/c36gMpRJkIk5F8hStyQoWwC/Rw= github.com/99designs/keyring v0.0.0-20190110203331-82da6802f65f h1:WXiWWJrYCaOaYimBAXlRdRJ7qOisrYyMLYnCvvhHVms= github.com/99designs/keyring v0.0.0-20190110203331-82da6802f65f/go.mod h1:aKt8W/yd91/xHY6ixZAJZ2vYbhr3pP8DcrvuGSGNPJk= @@ -32,10 +36,13 @@ github.com/awslabs/goformation/v4 v4.4.0 h1:MwHqnYX+ebauk3EmQX8WXirI8OmHg/Cg34dt github.com/awslabs/goformation/v4 v4.4.0/go.mod h1:MBDN7u1lMNDoehbFuO4uPvgwPeolTMA2TzX1yO6KlxI= github.com/b-b3rn4rd/gocfn v0.0.0-20180729083956-9f400ac88956 h1:AR0vL4FEL1H4yhjXYtz8RJZ5xWO1CBs8IladWiCV3aU= github.com/b-b3rn4rd/gocfn v0.0.0-20180729083956-9f400ac88956/go.mod h1:kOCOw4KbqX3dYcTLjMneEELuPQ9drjIRtGMpwqhBrak= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom8DBE9so9EBsM= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= @@ -47,16 +54,36 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a h1:mq+R6XEM6lJX5VlLyZIrUSP8tSuJp82xTK89hvBwJbU= github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-ini/ini v1.42.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww= +github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hashicorp/aws-sdk-go-base v0.4.0 h1:zH9hNUdsS+2G0zJaU85ul8D59BGnZBaKM+KMNPAHGwk= @@ -67,12 +94,16 @@ github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6K github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= @@ -120,6 +151,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b h1:jUK33OXuZP/l6babJtnLo1qsGvq6G9so9KMflGAm4YA= github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b/go.mod h1:8458kAagoME2+LN5//WxE71ysZ3B7r22fdgb7qVmXSY= @@ -149,26 +181,43 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2 github.com/xeipuuv/gojsonreference v0.0.0-20150808065054-e02fc20de94c/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20181112162635-ac52e6811b56/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8= golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20170809000501-1c05540f6879/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20191021144547-ec77196f6094 h1:5O4U9trLjNpuhpynaDsqwCk+Tw6seqJz1EbqbnzHrc8= golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170814044513-c84c1ab9fd18/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -176,15 +225,43 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170814122439-e56139fd9c5b/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.21.0 h1:zS+Q/CJJnVlXpXQVIz+lH0ZT2lBuT2ac7XD8Y/3w6hY= +google.golang.org/api v0.21.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200413115906-b5235f65be36 h1:j7CmVRD4Kec0+f8VuBAc2Ak2MFfXm5Q2/RxuJLL+76E= +google.golang.org/genproto v0.0.0-20200413115906-b5235f65be36/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.1 h1:C1QC6KzgSiLyBabDi87BbjaGreoRgGUF5nOyvfrAZ1k= +google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -198,3 +275,6 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/config/config.go b/internal/config/config.go index 5650f51..9d12c14 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,6 +26,7 @@ type Agent struct { // CertificateAuthority config for the sshrimp-ca lambda type CertificateAuthority struct { + Project string AccountID int Regions []string FunctionName string @@ -46,7 +47,7 @@ type SSHrimp struct { } // List of supported regions for the config wizard -var supportedAwsRegions = []string{ +var SupportedAwsRegions = []string{ "ap-east-1", "ap-northeast-1", "ap-northeast-2", @@ -67,6 +68,17 @@ var supportedAwsRegions = []string{ "us-west-2", } +var SupportedGcpRegions = []string{ + "europe-west1", + "europe-west2", + "europe-west3", + "us-central1", + "us-east1", + "us-east4", + "asia-northeast1", + "asia-east2", +} + var supportedExtensions = []string{ "no-agent-forwarding", "no-port-forwarding", @@ -202,7 +214,7 @@ func certificateAuthorityQuestions(config *SSHrimp) []*survey.Question { Message: "AWS Region:", Default: config.CertificateAuthority.Regions, Help: "Select multiple regions for high availability. Each region gets it's own Lambda function and KMS key.", - Options: supportedAwsRegions, + Options: SupportedAwsRegions, PageSize: 10, }, Validate: survey.Required, diff --git a/internal/signer/kms.go b/internal/signer/aws.go similarity index 82% rename from internal/signer/kms.go rename to internal/signer/aws.go index 2af5b2e..9fae14b 100644 --- a/internal/signer/kms.go +++ b/internal/signer/aws.go @@ -13,25 +13,25 @@ import ( ) // KMSSigner an AWS asymetric crypto signer -type KMSSigner struct { +type AWSSigner struct { crypto.Signer client kmsiface.KMSAPI key string } -// NewKMSSigner return a new instsance of KMSSigner -func NewKMSSigner(key string) *KMSSigner { +// NewKMSSigner return a new instsance of AWSSigner +func NewAWSSigner(key string) *AWSSigner { sess := session.Must(session.NewSession()) - return &KMSSigner{ + return &AWSSigner{ key: key, client: kms.New(sess), } } // Public returns the public key from KMS -func (s *KMSSigner) Public() crypto.PublicKey { +func (s *AWSSigner) Public() crypto.PublicKey { response, err := s.client.GetPublicKey(&kms.GetPublicKeyInput{ KeyId: &s.key, @@ -51,7 +51,7 @@ func (s *KMSSigner) Public() crypto.PublicKey { } // Sign a digest with the private key in KMS -func (s *KMSSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { +func (s *AWSSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { response, err := s.client.Sign(&kms.SignInput{ KeyId: &s.key, diff --git a/internal/signer/gcp.go b/internal/signer/gcp.go new file mode 100644 index 0000000..6945f0f --- /dev/null +++ b/internal/signer/gcp.go @@ -0,0 +1,89 @@ +package signer + +import ( + "context" + "crypto" + "crypto/x509" + "encoding/pem" + "fmt" + "io" + + kms "cloud.google.com/go/kms/apiv1" + kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" +) + +// KMSSigner a GCP asymetric crypto signer +type GCPSigner struct { + crypto.Signer + ctx context.Context + client *kms.KeyManagementClient + key string +} + +// NewGCPSSigner return a new instsance of NewGCPSSigner +func NewGCPSSigner(key string) *GCPSigner { + ctx := context.Background() + c, err := kms.NewKeyManagementClient(ctx) + if err != nil { + panic(err) + } + + return &GCPSigner{ + ctx: ctx, + client: c, + key: key, + } +} + +// Public returns the public key from KMS +func (s *GCPSigner) Public() crypto.PublicKey { + + response, err := s.client.GetPublicKey(s.ctx, &kmspb.GetPublicKeyRequest{ + Name: s.key, + }) + if err != nil { + fmt.Printf(err.Error()) + return nil + } + + pubPem := response.GetPem() + // pubAlg := response.GetAlgorithm() + pemBlock, _ := pem.Decode([]byte(pubPem)) + + publicKey, err := x509.ParsePKIXPublicKey(pemBlock.Bytes) + if err != nil { + fmt.Printf(err.Error()) + return nil + } + + return publicKey +} + +// Sign a digest with the private key in KMS +func (s *GCPSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + var dig *kmspb.Digest = &kmspb.Digest{} + switch opts { + case crypto.SHA256: + dig.Digest = &kmspb.Digest_Sha256{ + Sha256: digest, + } + case crypto.SHA384: + dig.Digest = &kmspb.Digest_Sha384{ + Sha384: digest, + } + case crypto.SHA512: + dig.Digest = &kmspb.Digest_Sha512{ + Sha512: digest, + } + } + + response, err := s.client.AsymmetricSign(s.ctx, &kmspb.AsymmetricSignRequest{ + Name: s.key, + Digest: dig, + }) + if err != nil { + return nil, err + } + + return response.GetSignature(), nil +} diff --git a/internal/signer/sshrimp.go b/internal/signer/sshrimp.go index 65d193f..256851e 100644 --- a/internal/signer/sshrimp.go +++ b/internal/signer/sshrimp.go @@ -1,14 +1,26 @@ package signer import ( + "bytes" + "crypto/rand" + "encoding/hex" "encoding/json" "fmt" + "io/ioutil" + "math" + "math/big" + "net/http" + "regexp" + "sort" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/lambda" "github.com/pkg/errors" "github.com/stoggi/sshrimp/internal/config" + "github.com/stoggi/sshrimp/internal/identity" + "golang.org/x/crypto/ssh" ) @@ -29,11 +41,19 @@ type SSHrimpEvent struct { // SignCertificateAllRegions iterate through each configured region if there is an error signing the certificate func SignCertificateAllRegions(publicKey ssh.PublicKey, token string, forceCommand string, c *config.SSHrimp) (*ssh.Certificate, error) { - var err error + var ( + err error + cert *ssh.Certificate + ) // Try each configured region before exiting if there is an error + for _, region := range c.CertificateAuthority.Regions { - cert, err := SignCertificateOneRegion(publicKey, token, forceCommand, region, c) + if i := sort.SearchStrings(config.SupportedAwsRegions, region); i < len(config.SupportedAwsRegions) && config.SupportedAwsRegions[i] == region { + cert, err = SignCertificateAWS(publicKey, token, forceCommand, region, c) + } else if i := sort.SearchStrings(config.SupportedGcpRegions, region); i < len(config.SupportedGcpRegions) && config.SupportedGcpRegions[i] == region { + cert, err = SignCertificateGCP(publicKey, token, forceCommand, region, c) + } if err == nil { return cert, nil } @@ -41,8 +61,54 @@ func SignCertificateAllRegions(publicKey ssh.PublicKey, token string, forceComma return nil, err } -// SignCertificateOneRegion given a public key, identity token and forceCommand, invoke the sshrimp-ca lambda function -func SignCertificateOneRegion(publicKey ssh.PublicKey, token string, forceCommand string, region string, c *config.SSHrimp) (*ssh.Certificate, error) { +// SignCertificateGCP given a public key, identity token and forceCommand, invoke the sshrimp-ca GCP function +func SignCertificateGCP(publicKey ssh.PublicKey, token string, forceCommand string, region string, c *config.SSHrimp) (*ssh.Certificate, error) { + + // Setup the JSON payload for the SSHrimp CA + payload, err := json.Marshal(SSHrimpEvent{ + PublicKey: string(ssh.MarshalAuthorizedKey(publicKey)), + Token: token, + ForceCommand: forceCommand, + }) + if err != nil { + return nil, err + } + + result, err := http.Post(fmt.Sprintf("https://%s-%s.cloudfunctions.net/%s", region, c.CertificateAuthority.Project, c.CertificateAuthority.FunctionName), "application/json", bytes.NewReader(payload)) + if err != nil { + return nil, errors.Wrap(err, "http post failed: "+err.Error()) + } + if result.StatusCode != 200 { + return nil, fmt.Errorf("sshrimp returned status code %d", result.StatusCode) + } + + resbody, err := ioutil.ReadAll(result.Body) + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve the response from sshrimp-ca") + } + + // Parse the result form the lambda to extract the certificate + sshrimpResult := SSHrimpResult{} + err = json.Unmarshal(resbody, &sshrimpResult) + if err != nil { + return nil, errors.Wrap(err, "failed to parse json response from sshrimp-ca") + } + + // These error types and messages can also come from the aws-sdk-go lambda handler + if sshrimpResult.ErrorType != "" || sshrimpResult.ErrorMessage != "" { + return nil, fmt.Errorf("%s: %s", sshrimpResult.ErrorType, sshrimpResult.ErrorMessage) + } + + // Parse the certificate received by sshrimp-ca + cert, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshrimpResult.Certificate)) + if err != nil { + return nil, err + } + return cert.(*ssh.Certificate), nil +} + +// SignCertificateAWS given a public key, identity token and forceCommand, invoke the sshrimp-ca lambda function +func SignCertificateAWS(publicKey ssh.PublicKey, token string, forceCommand string, region string, c *config.SSHrimp) (*ssh.Certificate, error) { // Create a lambdaService using the new temporary credentials for the role session := session.Must(session.NewSession(&aws.Config{ Region: aws.String(region), @@ -90,3 +156,99 @@ func SignCertificateOneRegion(publicKey ssh.PublicKey, token string, forceComman } return cert.(*ssh.Certificate), nil } + +func ValidateRequest(event SSHrimpEvent, c *config.SSHrimp, requestID string, functionID string) (ssh.Certificate, error) { + // Validate the user supplied public key + publicKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(event.PublicKey)) + if err != nil { + return ssh.Certificate{}, fmt.Errorf("unable to parse public key: %v", err) + } + + // Validate the user supplied identity token with the loaded configuration + i, err := identity.NewIdentity(c) + username, err := i.Validate(event.Token) + if err != nil { + return ssh.Certificate{}, err + } + + // Validate and add force commands or source address options + criticalOptions := make(map[string]string) + if regexp.MustCompile(c.CertificateAuthority.ForceCommandRegex).MatchString(event.ForceCommand) { + if event.ForceCommand != "" { + criticalOptions["force-command"] = event.ForceCommand + } + } else { + return ssh.Certificate{}, errors.New("forcecommand validation failed") + } + if regexp.MustCompile(c.CertificateAuthority.SourceAddressRegex).MatchString(event.SourceAddress) { + if event.SourceAddress != "" { + criticalOptions["source-address"] = event.SourceAddress + } + } else { + return ssh.Certificate{}, errors.New("sourceaddress validation failed") + } + + // Generate a random nonce for the certificate + bytes := make([]byte, 32) + nonce := make([]byte, len(bytes)*2) + if _, err := rand.Read(bytes); err != nil { + return ssh.Certificate{}, err + } + hex.Encode(nonce, bytes) + + // Generate a random serial number + serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + return ssh.Certificate{}, err + } + + // Validate and set the certificate valid and expire times + now := time.Now() + validAfterOffset, err := time.ParseDuration(c.CertificateAuthority.ValidAfterOffset) + if err != nil { + return ssh.Certificate{}, err + } + validBeforeOffset, err := time.ParseDuration(c.CertificateAuthority.ValidBeforeOffset) + if err != nil { + return ssh.Certificate{}, err + } + validAfter := now.Add(validAfterOffset) + validBefore := now.Add(validBeforeOffset) + + // Convert the extensions slice to a map + extensions := make(map[string]string, len(c.CertificateAuthority.Extensions)) + for _, extension := range c.CertificateAuthority.Extensions { + extensions[extension] = "" + } + + // Create a key ID to be added to the certificate. Follows BLESS Key ID format + // https://github.com/Netflix/bless + keyID := fmt.Sprintf("request[%s] for[%s] from[%s] command[%s] ssh_key[%s] ca[%s] valid_to[%s]", + requestID, + username, + event.SourceAddress, + event.ForceCommand, + ssh.FingerprintSHA256(publicKey), + functionID, + validBefore.Format("2006/01/02 15:04:05"), + ) + + // Create the certificate struct with all our configured alues + certificate := ssh.Certificate{ + Nonce: nonce, + Key: publicKey, + Serial: serial.Uint64(), + CertType: ssh.UserCert, + KeyId: keyID, + ValidPrincipals: []string{ + username, + }, + Permissions: ssh.Permissions{ + CriticalOptions: criticalOptions, + Extensions: extensions, + }, + ValidAfter: uint64(validAfter.Unix()), + ValidBefore: uint64(validBefore.Unix()), + } + return certificate, nil +} diff --git a/tools/mage/ca/ca.go b/tools/mage/ca/ca.go index d45f4f7..62645aa 100644 --- a/tools/mage/ca/ca.go +++ b/tools/mage/ca/ca.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "os" + "path/filepath" "strings" "github.com/aws/aws-sdk-go/aws" @@ -42,6 +43,15 @@ func Build() error { return sh.RunWith(env, "go", "build", "./cmd/sshrimp-ca") } +// Build Builds the GCP certificate authority +// It produces gcp.a for checking timestamps +func BuildGCP() error { + env := map[string]string{ + "GOOS": "linux", + } + return sh.RunWith(env, "go", "build", "-o", "gcp.a", "./gcp") +} + // Package Packages the certificate authority files into a zip archive func Package() error { if modified, err := target.Path("sshrimp-ca", config.GetPath()); err == nil && !modified { @@ -62,6 +72,33 @@ func Package() error { return nil } +// PackageGCP Packages the certificate authority files into a zip archive +func PackageGCP() error { + if modified, err := target.Path("gcp.a", config.GetPath()); err == nil && !modified { + return nil + } + + mg.Deps(BuildGCP, Config) + + zipFile, err := os.Create("sshrimp-ca-gcp.zip") + if err != nil { + return err + } + defer zipFile.Close() + + err = gcpCreateArchive(zipFile, []ZipFiles{ + ZipFiles{Filename: "go.mod"}, + {"gcp/gcp.go", "gcp.go"}, + ZipFiles{Filename: "internal"}, + ZipFiles{config.GetPath(), filepath.Base(config.GetPath())}, + }...) + + if err != nil { + return err + } + return nil +} + // Generate Generates a CloudFormation template used to deploy the certificate authority func Generate() error { if modified, err := target.Path("sshrimp-ca.tf.json", config.GetPath()); err == nil && !modified { @@ -139,5 +176,11 @@ func Clean() error { if err := sh.Rm("sshrimp-ca.zip"); err != nil { return err } + if err := sh.Rm("sshrimp-ca-gcp.zip"); err != nil { + return err + } + if err := sh.Rm("gcp.a"); err != nil { + return err + } return nil } diff --git a/tools/mage/ca/lambda.go b/tools/mage/ca/lambda.go index 98e9da7..6479410 100644 --- a/tools/mage/ca/lambda.go +++ b/tools/mage/ca/lambda.go @@ -4,8 +4,14 @@ import ( "archive/zip" "io" "os" + "path/filepath" + "strings" ) +type ZipFiles struct { + Filename, ZipPath string +} + // Packages the certificate authority lambda into a zip archive on writer func lambdaCreateArchive(wr io.Writer, filename ...string) error { @@ -41,3 +47,90 @@ func lambdaCreateArchive(wr io.Writer, filename ...string) error { return nil } + +// Packages the certificate authority function into a GCP compatible zip archive on writer +func gcpCreateArchive(wr io.Writer, files ...ZipFiles) error { + + archive := zip.NewWriter(wr) + defer archive.Close() + + for _, fileinfo := range files { + info, err := os.Stat(fileinfo.Filename) + if err != nil { + return err + } + if info.IsDir() { + err = zipDirectory(archive, fileinfo.Filename, fileinfo.ZipPath) + if err != nil { + return err + } + continue + } + + if strings.TrimSpace(fileinfo.ZipPath) == "" { + fileinfo.ZipPath = fileinfo.Filename + } + + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + header.Name = fileinfo.ZipPath + + writer, err := archive.CreateHeader(header) + if err != nil { + return err + } + + file, err := os.Open(fileinfo.Filename) + if err != nil { + return err + } + defer file.Close() + + if _, err := io.Copy(writer, file); err != nil { + return err + } + } + + return nil +} + +func zipDirectory(archive *zip.Writer, dir, zipDir string) error { + return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + var zipPath string + if strings.TrimSpace(zipDir) == "" { + zipPath = path + } else { + zipPath = strings.Replace(path, dir, zipDir, 1) + } + if info.IsDir() { + return nil + } + + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + header.Name = zipPath + + writer, err := archive.CreateHeader(header) + if err != nil { + return err + } + + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + if _, err := io.Copy(writer, file); err != nil { + return err + } + return nil + }) +}