diff --git a/.github/integration/scripts/charts/dependencies.sh b/.github/integration/scripts/charts/dependencies.sh index d0d752e35..d38df7c00 100644 --- a/.github/integration/scripts/charts/dependencies.sh +++ b/.github/integration/scripts/charts/dependencies.sh @@ -19,7 +19,6 @@ C4GHPASSPHRASE="$(random-string)" export C4GHPASSPHRASE crypt4gh generate -n c4gh -p "$C4GHPASSPHRASE" kubectl create secret generic c4gh --from-file="c4gh.sec.pem" --from-file="c4gh.pub.pem" --from-literal=passphrase="${C4GHPASSPHRASE}" - # secret for the OIDC keypair openssl ecparam -name prime256v1 -genkey -noout -out "jwt.key" openssl ec -in "jwt.key" -pubout -out "jwt.pub" diff --git a/.github/integration/scripts/charts/values.yaml b/.github/integration/scripts/charts/values.yaml index a33446f24..96639f753 100644 --- a/.github/integration/scripts/charts/values.yaml +++ b/.github/integration/scripts/charts/values.yaml @@ -48,7 +48,7 @@ global: secretName: c4gh keyFile: c4gh.sec.pem publicFile: c4gh.pub.pem - passphrase: PLACEHOLDER_VALUE + passphrase: PLACEHOLDER_VALUE db: host: "postgres-sda-db" user: "postgres" diff --git a/charts/sda-svc/README.md b/charts/sda-svc/README.md index 1e80ce536..77f8c45ac 100644 --- a/charts/sda-svc/README.md +++ b/charts/sda-svc/README.md @@ -95,7 +95,7 @@ Parameter | Description | Default `global.cega.password` | Password for the EGA user authentication service. |`""` `global.c4gh.keyFile` | Private C4GH key. |`c4gh.key` `global.c4gh.passphrase` | Passphrase for the private C4GH key. |`""` -`global.c4gh.publicFile` | Public key corresponding to the private key, neeeded for tests. |`""` +`global.c4gh.publicFile` | Public key corresponding to the private key, provided in /info endpoint. |`""` `global.db.host` | Hostname for the database. |`""` `global.db.name` | Database to connect to. |`lega` `global.db.passIngest` | Password used for `data in` services. |`""` diff --git a/charts/sda-svc/templates/auth-deploy.yaml b/charts/sda-svc/templates/auth-deploy.yaml index 12f47927e..f6011c88e 100644 --- a/charts/sda-svc/templates/auth-deploy.yaml +++ b/charts/sda-svc/templates/auth-deploy.yaml @@ -137,6 +137,8 @@ spec: {{- end }} - name: RESIGNJWT value: {{ .Values.global.auth.resignJwt | quote }} + - name: PUBLICFILE + value: "{{ template "c4ghPath" . }}/{{ .Values.global.c4gh.publicFile }}" {{- if .Values.global.tls.enabled}} - name: SERVER_CERT value: {{ template "tlsPath" . }}/tls.crt @@ -184,6 +186,10 @@ spec: - name: jwt mountPath: {{ template "jwtPath" . }} {{- end }} + {{- if not .Values.global.vaultSecrets }} + - name: c4gh + mountPath: {{ template "c4ghPath" . }} + {{- end }} volumes: {{- if and (.Values.global.auth.resignJwt) (not .Values.global.vaultSecrets) }} - name: jwt @@ -196,6 +202,15 @@ spec: - key: {{ required "The name of the JWT signing key is needed" .Values.global.auth.jwtKey }} path: {{ .Values.global.auth.jwtKey }} {{- end }} + {{- if not .Values.global.vaultSecrets }} + - name: c4gh + secret: + defaultMode: 0440 + secretName: {{ required "A secret for the C4GH public key is needed" .Values.global.c4gh.secretName }} + items: + - key: {{ required "The C4GH public key is needed" .Values.global.c4gh.publicFile }} + path: {{ .Values.global.c4gh.publicFile }} + {{- end }} {{- if and (not .Values.global.pkiService) .Values.global.tls.enabled }} - name: tls projected: diff --git a/sda-auth/config.go b/sda-auth/config.go index 6963808ec..c582e094b 100644 --- a/sda-auth/config.go +++ b/sda-auth/config.go @@ -55,6 +55,7 @@ type Config struct { ResignJwt bool InfoURL string InfoText string + PublicFile string } // NewConfig initializes and parses the config file and/or environment using @@ -94,6 +95,7 @@ func (c *Config) readConfig() error { c.JwtIssuer = viper.GetString("jwtIssuer") c.InfoURL = viper.GetString("infoUrl") c.InfoText = viper.GetString("infoText") + c.PublicFile = viper.GetString("publicFile") viper.SetDefault("ResignJwt", true) c.ResignJwt = viper.GetBool("resignJwt") @@ -172,8 +174,10 @@ func (c *Config) readConfig() error { log.Printf("Setting log level to '%s'", stringLevel) } - if viper.GetString("s3Inbox") == "" { - return fmt.Errorf("%s not set", "s3Inbox") + for _, s := range []string{"s3Inbox", "publicFile"} { + if viper.GetString(s) == "" { + return fmt.Errorf("%s not set", s) + } } // no need to check the variables for JWT generation if we won't use it diff --git a/sda-auth/config.yaml b/sda-auth/config.yaml index 5a74294b9..fc7c1cec3 100644 --- a/sda-auth/config.yaml +++ b/sda-auth/config.yaml @@ -24,3 +24,4 @@ jwtSignatureAlg: "ES256" resignJwt: true infoText: "About Federated EGA" infoUrl: "https://ega-archive.org/about/projects-and-funders/federated-ega/" +publicFile: "/keys/c4gh.pub.pem" diff --git a/sda-auth/config_test.go b/sda-auth/config_test.go index 208612120..ac5f36547 100644 --- a/sda-auth/config_test.go +++ b/sda-auth/config_test.go @@ -32,6 +32,7 @@ type ConfigTests struct { ResignJwt bool InfoURL string InfoText string + PublicFile string } func TestConfigTestSuite(t *testing.T) { @@ -89,6 +90,7 @@ func (suite *ConfigTests) SetupTest() { suite.ResignJwt = true suite.InfoURL = "https://test.info" suite.InfoText = "About LEGA" + suite.PublicFile = "public.pem" // Write config to temp config file configYaml, err := yaml.Marshal(Config{ @@ -102,6 +104,7 @@ func (suite *ConfigTests) SetupTest() { ResignJwt: suite.ResignJwt, InfoURL: suite.InfoURL, InfoText: suite.InfoText, + PublicFile: suite.PublicFile, }) if err != nil { log.Errorf("Error marshalling config yaml: %v", err) @@ -186,6 +189,7 @@ func (suite *ConfigTests) TestConfig() { os.Setenv("INFOTEXT", fmt.Sprintf("env_%v", suite.InfoText)) os.Setenv("INFOURL", fmt.Sprintf("env_%v", suite.InfoURL)) + os.Setenv("PUBLICFILE", fmt.Sprintf("env_%v", suite.PublicFile)) // re-read the config config, err = NewConfig() @@ -212,6 +216,7 @@ func (suite *ConfigTests) TestConfig() { assert.Equal(suite.T(), fmt.Sprintf("env_%v", suite.InfoText), config.InfoText, "Project info text misread from environment variable") assert.Equal(suite.T(), fmt.Sprintf("env_%v", suite.InfoURL), config.InfoURL, "Project info text misread from environment variable") + assert.Equal(suite.T(), fmt.Sprintf("env_%v", suite.PublicFile), config.PublicFile, "Public file misread from environment variable") // Check missing private key os.Setenv("JWTPRIVATEKEY", "nonexistent-key-file") diff --git a/sda-auth/dev-server/docker-compose.yml b/sda-auth/dev-server/docker-compose.yml index a1f2f246e..de51dbd48 100644 --- a/sda-auth/dev-server/docker-compose.yml +++ b/sda-auth/dev-server/docker-compose.yml @@ -43,6 +43,16 @@ services: - CEGA_USERS_USER=dummy ports: - 8443:8443 + keygen: + image: golang:alpine3.16 + container_name: keygen + command: + - "/bin/sh" + - "-c" + - if [ ! -f "/out/c4gh.sec.pem" ]; then wget -qO- "https://github.com/neicnordic/crypt4gh/releases/latest/download/crypt4gh_linux_x86_64.tar.gz" | tar zxf -; + ./crypt4gh generate -n c4gh -p privatekeypass && mv *.pem /out/; fi + volumes: + - /tmp:/out auth: container_name: auth build: @@ -55,6 +65,8 @@ services: condition: service_healthy cega: condition: service_started + keygen: + condition: service_completed_successfully environment: - ELIXIR_ID=XC56EL11xx - ELIXIR_PROVIDER=http://oidc:9090 @@ -73,9 +85,11 @@ services: - JWTSIGNATUREALG=ES256 - INFOTEXT=About Federated EGA - INFOURL=https://ega-archive.org/about/projects-and-funders/federated-ega/ + - PUBLICFILE=/c4gh.pub.pem volumes: - ../keys:/keys - ../:/sda-auth + - /tmp/c4gh.pub.pem:/c4gh.pub.pem image: sda-auth ports: - 8080:8080 diff --git a/sda-auth/go.mod b/sda-auth/go.mod index caa21e294..5f2083859 100644 --- a/sda-auth/go.mod +++ b/sda-auth/go.mod @@ -9,6 +9,7 @@ require ( github.com/iris-contrib/middleware/cors v0.0.0-20230311205048-b568fe9b470f github.com/kataras/iris/v12 v12.2.7 github.com/lestrrat/go-jwx v0.9.1 + github.com/neicnordic/crypt4gh v1.7.6 github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282 github.com/sirupsen/logrus v1.9.3 github.com/spf13/viper v1.18.2 @@ -20,6 +21,7 @@ require ( ) require ( + filippo.io/edwards25519 v1.0.0 // indirect github.com/BurntSushi/toml v1.3.2 // indirect github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/CloudyKit/jet/v6 v6.2.0 // indirect @@ -28,6 +30,7 @@ require ( github.com/andybalholm/brotli v1.0.5 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect github.com/fatih/structs v1.1.0 // indirect github.com/flosch/pongo2/v4 v4.0.2 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect diff --git a/sda-auth/go.sum b/sda-auth/go.sum index a572f88e9..608c1908c 100644 --- a/sda-auth/go.sum +++ b/sda-auth/go.sum @@ -1,3 +1,5 @@ +filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= +filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= @@ -21,6 +23,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU= +github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= @@ -96,6 +100,8 @@ github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/neicnordic/crypt4gh v1.7.6 h1:Vqcb8Yb950oaBBJFepDK1oLeu9rZzpywYWVHLmO0oI8= +github.com/neicnordic/crypt4gh v1.7.6/go.mod h1:rqmVXsprDFBRRLJkm1cK9kLETBPGEZmft9lHD/V40wk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282 h1:TQMyrpijtkFyXpNI3rY5hsZQZw+paiH+BfAlsb81HBY= @@ -202,6 +208,7 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/sda-auth/info.go b/sda-auth/info.go new file mode 100644 index 000000000..375bad2fe --- /dev/null +++ b/sda-auth/info.go @@ -0,0 +1,45 @@ +package main + +import ( + "os" + "path/filepath" + + "github.com/kataras/iris/v12" + "github.com/neicnordic/crypt4gh/keys" + log "github.com/sirupsen/logrus" +) + +type Info struct { + ClientID string `json:"client_id"` + OidcURI string `json:"oidc_uri"` + PublicKey string `json:"public_key"` + InboxURI string `json:"inbox_uri"` +} + +// Reads the public key file and returns the public key +func readPublicKeyFile(filename string) (key *[32]byte, err error) { + log.Info("Reading Public key file") + file, err := os.Open(filepath.Clean(filename)) + if err != nil { + return nil, err + } + defer file.Close() + publicKey, err := keys.ReadPublicKey(file) + if err != nil { + return nil, err + } + + return &publicKey, err +} + +// getInfo returns information needed by the client to authenticate +func (auth AuthHandler) getInfo(ctx iris.Context) { + info := Info{ClientID: auth.OAuth2Config.ClientID, OidcURI: auth.Config.Elixir.Provider, PublicKey: auth.pubKey, InboxURI: auth.Config.S3Inbox} + + err := ctx.JSON(info) + if err != nil { + log.Error("Failure to get Info ", err) + + return + } +} diff --git a/sda-auth/main.go b/sda-auth/main.go index 40aa2a1ed..a6cd508fd 100644 --- a/sda-auth/main.go +++ b/sda-auth/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/hex" "encoding/json" "fmt" "io" @@ -35,6 +36,7 @@ type AuthHandler struct { OIDCProvider *oidc.Provider htmlDir string staticDir string + pubKey string } func (auth AuthHandler) getInboxConfig(ctx iris.Context, authType string) { @@ -380,6 +382,7 @@ func main() { OIDCProvider: provider, htmlDir: "./frontend/templates", staticDir: "./frontend/static", + pubKey: "", } // Initialise web server @@ -417,6 +420,15 @@ func main() { app.Get("/elixir/login", authHandler.getElixirLogin) app.Get("/elixir/cors_login", authHandler.getElixirCORSLogin) + publicKey, err := readPublicKeyFile(authHandler.Config.PublicFile) + if err != nil { + log.Fatalf("Failed to get public key: %s", err.Error()) + } + authHandler.pubKey = hex.EncodeToString(publicKey[:]) + + // Endpoint for client login info + app.Get("/info", authHandler.getInfo) + app.UseGlobal(globalHeaders) if config.Server.Cert != "" && config.Server.Key != "" {