From 45673cb69c0b7830bb3a9530da13e03f21ac8ae1 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Tue, 8 Oct 2019 08:24:02 -0700 Subject: [PATCH 01/48] resolve conflicts --- .../openapitools/codegen/DefaultCodegen.java | 2 +- .../codegen/utils/ModelUtils.java | 11 +- .../resources/go-experimental/client.mustache | 102 ++++++++++++++++++ .../go-experimental/configuration.mustache | 38 +++++++ .../src/main/resources/go/client.mustache | 94 ++++++++++++++++ .../main/resources/go/configuration.mustache | 45 ++++++++ .../resources/python/requirements.mustache | 1 + .../src/main/resources/python/setup.mustache | 2 +- 8 files changed, 291 insertions(+), 4 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index f29243ef1516..5cde4fa7ad4b 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -3076,7 +3076,7 @@ public CodegenParameter fromParameter(Parameter parameter, Set imports) } else if (parameter.getContent() != null) { Content content = parameter.getContent(); if (content.size() > 1) { - LOGGER.warn("Multiple schemas found in content, returning only the first one"); + LOGGER.warn("Multiple schemas (size=" + content.size() + ") found in content, returning only the first one. Parameter: " + parameter.getName()); } MediaType mediaType = content.values().iterator().next(); s = mediaType.getSchema(); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index 26b0b6539a9a..aca490d0379c 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -778,10 +778,17 @@ private static Schema getSchemaFromContent(Content content) { if (content == null || content.isEmpty()) { return null; } - if (content.size() > 1) { - LOGGER.warn("Multiple schemas found in content, returning only the first one"); + for (Entry e : content.entrySet()) { + if (e.getKey().equals("application/json") || e.getKey().equals("application/xml")) { + // When there are multiple types, prefer for "application/json" or "application/xml" to make it more deterministic. + LOGGER.warn("Multiple schemas found in content, returning: " + e.getKey()); + return e.getValue().getSchema(); + } } MediaType mediaType = content.values().iterator().next(); + if (content.size() > 1) { + LOGGER.warn("Multiple schemas found in content, returning only the first one: " + content.keySet().iterator().next()); + } return mediaType.getSchema(); } diff --git a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache index ec129d3d4323..1ccce0b40405 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache @@ -4,6 +4,10 @@ package {{packageName}} import ( "bytes" "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "encoding/base64" "encoding/json" "encoding/xml" "errors" @@ -175,6 +179,94 @@ func (c *APIClient) GetConfig() *Configuration { return c.cfg } +// signRequest signs the request using HTTP signature. +// See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ +func (c *APIClient) signRequest( + ctx context.Context, + r *http.Request, + auth HttpSignatureAuth) error { + + if auth.PrivateKey == nil { + return errors.New("Private key is not set") + } + date := time.Now().UTC().Format(http.TimeFormat) + var digest string + var h crypto.Hash + var err error + switch auth.Algorithm { + case "rsa-sha512", "hs2019": + h = crypto.SHA512 + digest = "SHA-512=" + case "rsa-sha256": + // This is deprecated and should not longer be used. + h = crypto.SHA256 + digest = "SHA-256=" + default: + return fmt.Errorf("Unsupported signature algorith: %v", auth.Algorithm) + } + if !h.Available() { + return fmt.Errorf("Hash '%v' is not available", h) + } + // Calculate body digest per RFC 3230 section 4.3.2 + bodyHash := h.New() + if r.Body != nil { + if _, err = io.Copy(bodyHash, r.Body); err != nil { + return err + } + } + d := bodyHash.Sum(nil) + digest = digest + base64.StdEncoding.EncodeToString(d) + + // Build the string to be signed. + var sb strings.Builder + // TODO: According to the proposed RFC, the value of the header should not be ToLower + fmt.Fprintf(&sb, "(request-target): %s %s", strings.ToLower(r.Method), strings.ToLower(r.URL.EscapedPath())) + if r.URL.RawQuery != "" { + // The ":path" pseudo-header field includes the path and query parts + // of the target URI (the "path-absolute" production and optionally a + // '?' character followed by the "query" production (see Sections 3.3 + // and 3.4 of [RFC3986] + fmt.Fprintf(&sb, "?%s", strings.ToLower(r.URL.RawQuery)) + } + fmt.Fprintf(&sb, "\ndate: %s", date) + fmt.Fprintf(&sb, "\nhost: %s", r.Host) + fmt.Fprintf(&sb, "\ndigest: %s", digest) + for _, header := range auth.SignedHeaders { + fmt.Fprintf(&sb, "\n%s: %s", strings.ToLower(header), r.Header.Get(header)) + } + + msgHash := h.New() + if _, err = msgHash.Write([]byte(sb.String())); err != nil { + return err + } + d = msgHash.Sum(nil) + + var signature []byte + switch auth.PrivateKey.(type) { + case *rsa.PrivateKey: + signature, err = rsa.SignPKCS1v15(rand.Reader, auth.PrivateKey.(*rsa.PrivateKey), h, d) + default: + return fmt.Errorf("Unsupported private key") + } + if err != nil { + return err + } + + sb.Reset() + sb.WriteString("(request-target) date host digest") + for _, h := range auth.SignedHeaders { + sb.WriteRune(' ') + sb.WriteString(strings.ToLower(h)) + } + r.Header.Set("Host", r.Host) + r.Header.Set("Date", date) + r.Header.Set("Digest", digest) + authStr := fmt.Sprintf(`Signature keyId="%s", algorithm="%s", headers="%s", signature="%s"`, + auth.KeyId, auth.Algorithm, sb.String(), base64.StdEncoding.EncodeToString(signature)) + r.Header.Set("Authorization", authStr) + return nil +} + // prepareRequest build the request func (c *APIClient) prepareRequest( ctx context.Context, @@ -335,6 +427,16 @@ func (c *APIClient) prepareRequest( localVarRequest.Header.Add(header, value) } + if ctx != nil { + // HTTP Signature Authentication. All request headers must be set (including default headers) + // because the headers may be included in the signature. + if auth, ok := ctx.Value(ContextHttpSignatureAuth).(HttpSignatureAuth); ok { + err = c.signRequest(ctx, localVarRequest, auth) + if err != nil { + return nil, err + } + } + } return localVarRequest, nil } diff --git a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache index 0b74d83401f7..401dcf8cfb4c 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache @@ -2,7 +2,12 @@ package {{packageName}} import ( + "crypto" + "crypto/x509" + "encoding/pem" + "io/ioutil" "net/http" + "os" ) // contextKeys are used to identify the type of value in the context. @@ -27,6 +32,9 @@ var ( // ContextAPIKeys takes a string apikey as authentication for the request ContextAPIKeys = contextKey("apiKeys") + + // ContextHttpSignatureAuth takes HttpSignatureAuth as authentication for the request. + ContextHttpSignatureAuth = contextKey("httpsignature") ) // BasicAuth provides basic http authentication to a request passed via context using ContextBasicAuth @@ -41,6 +49,36 @@ type APIKey struct { Prefix string } +// HttpSignatureAuth provides http message signature authentication to a request passed via context using ContextHttpSignatureAuth +type HttpSignatureAuth struct { + KeyId string // A key identifier. + PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. + Algorithm string // The signature algorithm. Supported values are rsa-sha256, rsa-sha512, hs2019. + SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. +} + +// LoadPrivateKey reads the private key from the specified file. +func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { + var file *os.File + file, err = os.Open(filename) + if err != nil { + return err + } + defer func() { + err = file.Close() + }() + var priv []byte + priv, err = ioutil.ReadAll(file) + if err != nil { + return err + } + privPem, _ := pem.Decode(priv) + if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privPem.Bytes); err != nil { + return err + } + return nil +} + // Configuration stores the configuration of the API client type Configuration struct { BasePath string `json:"basePath,omitempty"` diff --git a/modules/openapi-generator/src/main/resources/go/client.mustache b/modules/openapi-generator/src/main/resources/go/client.mustache index ec129d3d4323..f21cfbc6f767 100644 --- a/modules/openapi-generator/src/main/resources/go/client.mustache +++ b/modules/openapi-generator/src/main/resources/go/client.mustache @@ -4,6 +4,10 @@ package {{packageName}} import ( "bytes" "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "encoding/base64" "encoding/json" "encoding/xml" "errors" @@ -175,6 +179,86 @@ func (c *APIClient) GetConfig() *Configuration { return c.cfg } +// signRequest signs the request using HTTP signature. +// See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ +func (c *APIClient) signRequest( + ctx context.Context, + r *http.Request, + auth HttpSignatureAuth) error { + + if auth.PrivateKey == nil { + return errors.New("Private key is not set") + } + date := time.Now().UTC().Format(http.TimeFormat) + var digest string + var h crypto.Hash + var err error + switch auth.Algorithm { + case "rsa-sha512", "hs2019": + h = crypto.SHA512 + digest = "SHA-512=" + case "rsa-sha256": + // This is deprecated and should not longer be used. + h = crypto.SHA256 + digest = "SHA-256=" + default: + return fmt.Errorf("Unsupported signature algorith: %v", auth.Algorithm) + } + if !h.Available() { + return fmt.Errorf("Hash '%v' is not available", h) + } + bodyHash := h.New() + if r.Body != nil { + if _, err = io.Copy(bodyHash, r.Body); err != nil { + return err + } + } + d := bodyHash.Sum(nil) + digest = digest + base64.StdEncoding.EncodeToString(d) + + // Building the string to be signed. + var sb strings.Builder + // TODO: According to the proposed RFC, the value of the header should not be ToLower + fmt.Fprintf(&sb, "(request-target): %s %s", strings.ToLower(r.Method), strings.ToLower(r.URL.EscapedPath() + "?" + r.URL.RawQuery)) + for _, header := range auth.SignedHeaders { + fmt.Fprintf(&sb, "\n%s: %s", strings.ToLower(header), r.Header.Get(header)) + } + fmt.Fprintf(&sb, "\ndate: %s", date) + fmt.Fprintf(&sb, "\nhost: %s", r.Host) + fmt.Fprintf(&sb, "\ndigest: %s", digest) + + msgHash := h.New() + if _, err = msgHash.Write([]byte(sb.String())); err != nil { + return err + } + d = msgHash.Sum(nil) + + var signature []byte + switch auth.PrivateKey.(type) { + case *rsa.PrivateKey: + signature, err = rsa.SignPKCS1v15(rand.Reader, auth.PrivateKey.(*rsa.PrivateKey), h, d) + default: + return fmt.Errorf("Unsupported private key") + } + if err != nil { + return err + } + + sb.Reset() + sb.WriteString("(request-target)") + if len(auth.SignedHeaders) > 0 { + sb.WriteString(" " + strings.Join(auth.SignedHeaders, " ")) + } + sb.WriteString(" date host digest") + authStr := fmt.Sprintf(`Signature keyId="%s", algorithm="%s", headers="%s", signature="%s"`, + auth.KeyId, auth.Algorithm, sb.String(), base64.StdEncoding.EncodeToString(signature)) + r.Header.Set("Host", r.Host) + r.Header.Set("Date", date) + r.Header.Set("Digest", digest) + r.Header.Set("Authorization", authStr) + return nil +} + // prepareRequest build the request func (c *APIClient) prepareRequest( ctx context.Context, @@ -335,6 +419,16 @@ func (c *APIClient) prepareRequest( localVarRequest.Header.Add(header, value) } + if ctx != nil { + // HTTP Signature Authentication. All request headers must be set (including default headers) + // because the headers may be included in the signature. + if auth, ok := ctx.Value(ContextHttpSignatureAuth).(HttpSignatureAuth); ok { + err = c.signRequest(ctx, localVarRequest, auth) + if err != nil { + return nil, err + } + } + } return localVarRequest, nil } diff --git a/modules/openapi-generator/src/main/resources/go/configuration.mustache b/modules/openapi-generator/src/main/resources/go/configuration.mustache index 98f9d8fee26f..5fb955cb22d3 100644 --- a/modules/openapi-generator/src/main/resources/go/configuration.mustache +++ b/modules/openapi-generator/src/main/resources/go/configuration.mustache @@ -2,7 +2,12 @@ package {{packageName}} import ( + "crypto" + "crypto/x509" + "encoding/pem" + "io/ioutil" "net/http" + "os" ) // contextKeys are used to identify the type of value in the context. @@ -27,6 +32,9 @@ var ( // ContextAPIKey takes an APIKey as authentication for the request ContextAPIKey = contextKey("apikey") + + // ContextHttpSignatureAuth takes HttpSignatureAuth as authentication for the request. + ContextHttpSignatureAuth = contextKey("httpsignature") ) // BasicAuth provides basic http authentication to a request passed via context using ContextBasicAuth @@ -35,12 +43,49 @@ type BasicAuth struct { Password string `json:"password,omitempty"` } +type CryptoSignatureAlg int + +const ( + SignHs2019 CryptoSignatureAlg = iota // RSA-SHA512 + SignRsaSha256 // RSA-SHA256, deprecated +) + // APIKey provides API key based authentication to a request passed via context using ContextAPIKey type APIKey struct { Key string Prefix string } +// HttpSignatureAuth provides http message signature authentication to a request passed via context using ContextHttpSignatureAuth +type HttpSignatureAuth struct { + KeyId string // A key identifier. + PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. + Algorithm string // The signature algorithm. Supported values are rsa-sha256, rsa-sha512, hs2019. + SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. +} + +// LoadPrivateKey reads the private key from the specified file. +func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { + var file *os.File + file, err = os.Open(filename) + if err != nil { + return err + } + defer func() { + err = file.Close() + }() + var priv []byte + priv, err = ioutil.ReadAll(file) + if err != nil { + return err + } + privPem, _ := pem.Decode(priv) + if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privPem.Bytes); err != nil { + return err + } + return nil +} + // Configuration stores the configuration of the API client type Configuration struct { BasePath string `json:"basePath,omitempty"` diff --git a/modules/openapi-generator/src/main/resources/python/requirements.mustache b/modules/openapi-generator/src/main/resources/python/requirements.mustache index eb358efd5bd3..47e5f76d83a0 100644 --- a/modules/openapi-generator/src/main/resources/python/requirements.mustache +++ b/modules/openapi-generator/src/main/resources/python/requirements.mustache @@ -4,3 +4,4 @@ six >= 1.10 python_dateutil >= 2.5.3 setuptools >= 21.0.0 urllib3 >= 1.15.1 +pycryptodome >= 3.6.1 diff --git a/modules/openapi-generator/src/main/resources/python/setup.mustache b/modules/openapi-generator/src/main/resources/python/setup.mustache index a41fee146742..a5a642503992 100644 --- a/modules/openapi-generator/src/main/resources/python/setup.mustache +++ b/modules/openapi-generator/src/main/resources/python/setup.mustache @@ -16,7 +16,7 @@ VERSION = "{{packageVersion}}" # prerequisite: setuptools # http://pypi.python.org/pypi/setuptools -REQUIRES = ["urllib3 >= 1.15", "six >= 1.10", "certifi", "python-dateutil"] +REQUIRES = ["pycryptodome >= 3.6.1", "urllib3 >= 1.15", "six >= 1.10", "certifi", "python-dateutil"] {{#asyncio}} REQUIRES.append("aiohttp >= 3.0.0") {{/asyncio}} From 255a424f6519f35c934ae832a13c6fa8fa17149c Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Fri, 6 Dec 2019 08:22:05 -0800 Subject: [PATCH 02/48] revert. set a preference for application/json content-type. Moved to separate branch --- .../java/org/openapitools/codegen/DefaultCodegen.java | 2 +- .../org/openapitools/codegen/utils/ModelUtils.java | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 5cde4fa7ad4b..f29243ef1516 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -3076,7 +3076,7 @@ public CodegenParameter fromParameter(Parameter parameter, Set imports) } else if (parameter.getContent() != null) { Content content = parameter.getContent(); if (content.size() > 1) { - LOGGER.warn("Multiple schemas (size=" + content.size() + ") found in content, returning only the first one. Parameter: " + parameter.getName()); + LOGGER.warn("Multiple schemas found in content, returning only the first one"); } MediaType mediaType = content.values().iterator().next(); s = mediaType.getSchema(); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index aca490d0379c..26b0b6539a9a 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -778,17 +778,10 @@ private static Schema getSchemaFromContent(Content content) { if (content == null || content.isEmpty()) { return null; } - for (Entry e : content.entrySet()) { - if (e.getKey().equals("application/json") || e.getKey().equals("application/xml")) { - // When there are multiple types, prefer for "application/json" or "application/xml" to make it more deterministic. - LOGGER.warn("Multiple schemas found in content, returning: " + e.getKey()); - return e.getValue().getSchema(); - } - } - MediaType mediaType = content.values().iterator().next(); if (content.size() > 1) { - LOGGER.warn("Multiple schemas found in content, returning only the first one: " + content.keySet().iterator().next()); + LOGGER.warn("Multiple schemas found in content, returning only the first one"); } + MediaType mediaType = content.values().iterator().next(); return mediaType.getSchema(); } From 23b895b2f1211e69e02ecb3995a2f076f174566d Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Sat, 7 Dec 2019 08:47:25 -0800 Subject: [PATCH 03/48] do not convert path to lowercase --- .../src/main/resources/go-experimental/client.mustache | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache index 1ccce0b40405..444242dad9f5 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache @@ -219,14 +219,13 @@ func (c *APIClient) signRequest( // Build the string to be signed. var sb strings.Builder - // TODO: According to the proposed RFC, the value of the header should not be ToLower - fmt.Fprintf(&sb, "(request-target): %s %s", strings.ToLower(r.Method), strings.ToLower(r.URL.EscapedPath())) + fmt.Fprintf(&sb, "(request-target): %s %s", strings.ToLower(r.Method), r.URL.EscapedPath()) if r.URL.RawQuery != "" { // The ":path" pseudo-header field includes the path and query parts // of the target URI (the "path-absolute" production and optionally a // '?' character followed by the "query" production (see Sections 3.3 // and 3.4 of [RFC3986] - fmt.Fprintf(&sb, "?%s", strings.ToLower(r.URL.RawQuery)) + fmt.Fprintf(&sb, "?%s", r.URL.RawQuery) } fmt.Fprintf(&sb, "\ndate: %s", date) fmt.Fprintf(&sb, "\nhost: %s", r.Host) From 25d6b31e256cb3cc960402e80a882200fc4d2ec1 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Mon, 16 Dec 2019 09:50:33 -0800 Subject: [PATCH 04/48] add support for ecdsa keys --- .../resources/go-experimental/client.mustache | 13 ++- .../go-experimental/configuration.mustache | 13 ++- .../src/main/resources/go/client.mustache | 94 ------------------- .../main/resources/go/configuration.mustache | 45 --------- 4 files changed, 20 insertions(+), 145 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache index 444242dad9f5..71410f2920e3 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache @@ -7,6 +7,7 @@ import ( "crypto" "crypto/rand" "crypto/rsa" + "crypto/ecdsa" "encoding/base64" "encoding/json" "encoding/xml" @@ -233,17 +234,21 @@ func (c *APIClient) signRequest( for _, header := range auth.SignedHeaders { fmt.Fprintf(&sb, "\n%s: %s", strings.ToLower(header), r.Header.Get(header)) } - + msg := []byte(sb.String()) msgHash := h.New() - if _, err = msgHash.Write([]byte(sb.String())); err != nil { + if _, err = msgHash.Write(msg); err != nil { return err } d = msgHash.Sum(nil) var signature []byte - switch auth.PrivateKey.(type) { + switch key := auth.PrivateKey.(type) { case *rsa.PrivateKey: - signature, err = rsa.SignPKCS1v15(rand.Reader, auth.PrivateKey.(*rsa.PrivateKey), h, d) + signature, err = rsa.SignPKCS1v15(rand.Reader, key, h, d) + case *ecdsa.PrivateKey: + signature, err = key.Sign(rand.Reader, d, h) + //case ed25519.PrivateKey: requires go 1.13 + // signature, err = key.Sign(rand.Reader, msg, crypto.Hash(0)) default: return fmt.Errorf("Unsupported private key") } diff --git a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache index 401dcf8cfb4c..4a56f57c561a 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache @@ -73,8 +73,17 @@ func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { return err } privPem, _ := pem.Decode(priv) - if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privPem.Bytes); err != nil { - return err + switch privPem.Type { + case "RSA PRIVATE KEY": + if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privPem.Bytes); err != nil { + return err + } + case "ECDSA PRIVATE KEY": + if h.PrivateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil { + return err + } + default: + return fmt.Errorf("Key '%s' is not supported", privPem.Type) } return nil } diff --git a/modules/openapi-generator/src/main/resources/go/client.mustache b/modules/openapi-generator/src/main/resources/go/client.mustache index f21cfbc6f767..ec129d3d4323 100644 --- a/modules/openapi-generator/src/main/resources/go/client.mustache +++ b/modules/openapi-generator/src/main/resources/go/client.mustache @@ -4,10 +4,6 @@ package {{packageName}} import ( "bytes" "context" - "crypto" - "crypto/rand" - "crypto/rsa" - "encoding/base64" "encoding/json" "encoding/xml" "errors" @@ -179,86 +175,6 @@ func (c *APIClient) GetConfig() *Configuration { return c.cfg } -// signRequest signs the request using HTTP signature. -// See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ -func (c *APIClient) signRequest( - ctx context.Context, - r *http.Request, - auth HttpSignatureAuth) error { - - if auth.PrivateKey == nil { - return errors.New("Private key is not set") - } - date := time.Now().UTC().Format(http.TimeFormat) - var digest string - var h crypto.Hash - var err error - switch auth.Algorithm { - case "rsa-sha512", "hs2019": - h = crypto.SHA512 - digest = "SHA-512=" - case "rsa-sha256": - // This is deprecated and should not longer be used. - h = crypto.SHA256 - digest = "SHA-256=" - default: - return fmt.Errorf("Unsupported signature algorith: %v", auth.Algorithm) - } - if !h.Available() { - return fmt.Errorf("Hash '%v' is not available", h) - } - bodyHash := h.New() - if r.Body != nil { - if _, err = io.Copy(bodyHash, r.Body); err != nil { - return err - } - } - d := bodyHash.Sum(nil) - digest = digest + base64.StdEncoding.EncodeToString(d) - - // Building the string to be signed. - var sb strings.Builder - // TODO: According to the proposed RFC, the value of the header should not be ToLower - fmt.Fprintf(&sb, "(request-target): %s %s", strings.ToLower(r.Method), strings.ToLower(r.URL.EscapedPath() + "?" + r.URL.RawQuery)) - for _, header := range auth.SignedHeaders { - fmt.Fprintf(&sb, "\n%s: %s", strings.ToLower(header), r.Header.Get(header)) - } - fmt.Fprintf(&sb, "\ndate: %s", date) - fmt.Fprintf(&sb, "\nhost: %s", r.Host) - fmt.Fprintf(&sb, "\ndigest: %s", digest) - - msgHash := h.New() - if _, err = msgHash.Write([]byte(sb.String())); err != nil { - return err - } - d = msgHash.Sum(nil) - - var signature []byte - switch auth.PrivateKey.(type) { - case *rsa.PrivateKey: - signature, err = rsa.SignPKCS1v15(rand.Reader, auth.PrivateKey.(*rsa.PrivateKey), h, d) - default: - return fmt.Errorf("Unsupported private key") - } - if err != nil { - return err - } - - sb.Reset() - sb.WriteString("(request-target)") - if len(auth.SignedHeaders) > 0 { - sb.WriteString(" " + strings.Join(auth.SignedHeaders, " ")) - } - sb.WriteString(" date host digest") - authStr := fmt.Sprintf(`Signature keyId="%s", algorithm="%s", headers="%s", signature="%s"`, - auth.KeyId, auth.Algorithm, sb.String(), base64.StdEncoding.EncodeToString(signature)) - r.Header.Set("Host", r.Host) - r.Header.Set("Date", date) - r.Header.Set("Digest", digest) - r.Header.Set("Authorization", authStr) - return nil -} - // prepareRequest build the request func (c *APIClient) prepareRequest( ctx context.Context, @@ -419,16 +335,6 @@ func (c *APIClient) prepareRequest( localVarRequest.Header.Add(header, value) } - if ctx != nil { - // HTTP Signature Authentication. All request headers must be set (including default headers) - // because the headers may be included in the signature. - if auth, ok := ctx.Value(ContextHttpSignatureAuth).(HttpSignatureAuth); ok { - err = c.signRequest(ctx, localVarRequest, auth) - if err != nil { - return nil, err - } - } - } return localVarRequest, nil } diff --git a/modules/openapi-generator/src/main/resources/go/configuration.mustache b/modules/openapi-generator/src/main/resources/go/configuration.mustache index 5fb955cb22d3..98f9d8fee26f 100644 --- a/modules/openapi-generator/src/main/resources/go/configuration.mustache +++ b/modules/openapi-generator/src/main/resources/go/configuration.mustache @@ -2,12 +2,7 @@ package {{packageName}} import ( - "crypto" - "crypto/x509" - "encoding/pem" - "io/ioutil" "net/http" - "os" ) // contextKeys are used to identify the type of value in the context. @@ -32,9 +27,6 @@ var ( // ContextAPIKey takes an APIKey as authentication for the request ContextAPIKey = contextKey("apikey") - - // ContextHttpSignatureAuth takes HttpSignatureAuth as authentication for the request. - ContextHttpSignatureAuth = contextKey("httpsignature") ) // BasicAuth provides basic http authentication to a request passed via context using ContextBasicAuth @@ -43,49 +35,12 @@ type BasicAuth struct { Password string `json:"password,omitempty"` } -type CryptoSignatureAlg int - -const ( - SignHs2019 CryptoSignatureAlg = iota // RSA-SHA512 - SignRsaSha256 // RSA-SHA256, deprecated -) - // APIKey provides API key based authentication to a request passed via context using ContextAPIKey type APIKey struct { Key string Prefix string } -// HttpSignatureAuth provides http message signature authentication to a request passed via context using ContextHttpSignatureAuth -type HttpSignatureAuth struct { - KeyId string // A key identifier. - PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. - Algorithm string // The signature algorithm. Supported values are rsa-sha256, rsa-sha512, hs2019. - SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. -} - -// LoadPrivateKey reads the private key from the specified file. -func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { - var file *os.File - file, err = os.Open(filename) - if err != nil { - return err - } - defer func() { - err = file.Close() - }() - var priv []byte - priv, err = ioutil.ReadAll(file) - if err != nil { - return err - } - privPem, _ := pem.Decode(priv) - if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privPem.Bytes); err != nil { - return err - } - return nil -} - // Configuration stores the configuration of the API client type Configuration struct { BasePath string `json:"basePath,omitempty"` From 136f6b6b1a17ec6b370e349c1a175fae89eb22fe Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Mon, 16 Dec 2019 22:46:50 -0800 Subject: [PATCH 05/48] fix issue parsing ECDSA private key --- .../src/main/resources/go-experimental/configuration.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache index 4a56f57c561a..78b293181c3a 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache @@ -79,7 +79,7 @@ func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { return err } case "ECDSA PRIVATE KEY": - if h.PrivateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil { + if h.PrivateKey, err = x509.ParsePKCS8PrivateKey(privPem.Bytes); err != nil { return err } default: From 4ca55cefb38d27257854cb171d49ffd3f39fe4ad Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Mon, 13 Jan 2020 12:09:01 -0800 Subject: [PATCH 06/48] remove unrelated python changes --- .../src/main/resources/python/requirements.mustache | 1 - .../openapi-generator/src/main/resources/python/setup.mustache | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/python/requirements.mustache b/modules/openapi-generator/src/main/resources/python/requirements.mustache index 47e5f76d83a0..eb358efd5bd3 100644 --- a/modules/openapi-generator/src/main/resources/python/requirements.mustache +++ b/modules/openapi-generator/src/main/resources/python/requirements.mustache @@ -4,4 +4,3 @@ six >= 1.10 python_dateutil >= 2.5.3 setuptools >= 21.0.0 urllib3 >= 1.15.1 -pycryptodome >= 3.6.1 diff --git a/modules/openapi-generator/src/main/resources/python/setup.mustache b/modules/openapi-generator/src/main/resources/python/setup.mustache index a5a642503992..a41fee146742 100644 --- a/modules/openapi-generator/src/main/resources/python/setup.mustache +++ b/modules/openapi-generator/src/main/resources/python/setup.mustache @@ -16,7 +16,7 @@ VERSION = "{{packageVersion}}" # prerequisite: setuptools # http://pypi.python.org/pypi/setuptools -REQUIRES = ["pycryptodome >= 3.6.1", "urllib3 >= 1.15", "six >= 1.10", "certifi", "python-dateutil"] +REQUIRES = ["urllib3 >= 1.15", "six >= 1.10", "certifi", "python-dateutil"] {{#asyncio}} REQUIRES.append("aiohttp >= 3.0.0") {{/asyncio}} From 0d6f013b48e0dc9ecafa7f266bf7761ab9b82f15 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Mon, 13 Jan 2020 13:16:07 -0800 Subject: [PATCH 07/48] Run go scripts under ./bin --- .../go-experimental/go-petstore/client.go | 106 ++++++++++++++++++ .../go-petstore/configuration.go | 47 ++++++++ 2 files changed, 153 insertions(+) diff --git a/samples/client/petstore/go-experimental/go-petstore/client.go b/samples/client/petstore/go-experimental/go-petstore/client.go index 4c4ecf168f41..5e826a8627a6 100644 --- a/samples/client/petstore/go-experimental/go-petstore/client.go +++ b/samples/client/petstore/go-experimental/go-petstore/client.go @@ -12,6 +12,11 @@ package petstore import ( "bytes" "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/ecdsa" + "encoding/base64" "encoding/json" "encoding/xml" "errors" @@ -203,6 +208,97 @@ func (c *APIClient) GetConfig() *Configuration { return c.cfg } +// signRequest signs the request using HTTP signature. +// See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ +func (c *APIClient) signRequest( + ctx context.Context, + r *http.Request, + auth HttpSignatureAuth) error { + + if auth.PrivateKey == nil { + return errors.New("Private key is not set") + } + date := time.Now().UTC().Format(http.TimeFormat) + var digest string + var h crypto.Hash + var err error + switch auth.Algorithm { + case "rsa-sha512", "hs2019": + h = crypto.SHA512 + digest = "SHA-512=" + case "rsa-sha256": + // This is deprecated and should not longer be used. + h = crypto.SHA256 + digest = "SHA-256=" + default: + return fmt.Errorf("Unsupported signature algorith: %v", auth.Algorithm) + } + if !h.Available() { + return fmt.Errorf("Hash '%v' is not available", h) + } + // Calculate body digest per RFC 3230 section 4.3.2 + bodyHash := h.New() + if r.Body != nil { + if _, err = io.Copy(bodyHash, r.Body); err != nil { + return err + } + } + d := bodyHash.Sum(nil) + digest = digest + base64.StdEncoding.EncodeToString(d) + + // Build the string to be signed. + var sb strings.Builder + fmt.Fprintf(&sb, "(request-target): %s %s", strings.ToLower(r.Method), r.URL.EscapedPath()) + if r.URL.RawQuery != "" { + // The ":path" pseudo-header field includes the path and query parts + // of the target URI (the "path-absolute" production and optionally a + // '?' character followed by the "query" production (see Sections 3.3 + // and 3.4 of [RFC3986] + fmt.Fprintf(&sb, "?%s", r.URL.RawQuery) + } + fmt.Fprintf(&sb, "\ndate: %s", date) + fmt.Fprintf(&sb, "\nhost: %s", r.Host) + fmt.Fprintf(&sb, "\ndigest: %s", digest) + for _, header := range auth.SignedHeaders { + fmt.Fprintf(&sb, "\n%s: %s", strings.ToLower(header), r.Header.Get(header)) + } + msg := []byte(sb.String()) + msgHash := h.New() + if _, err = msgHash.Write(msg); err != nil { + return err + } + d = msgHash.Sum(nil) + + var signature []byte + switch key := auth.PrivateKey.(type) { + case *rsa.PrivateKey: + signature, err = rsa.SignPKCS1v15(rand.Reader, key, h, d) + case *ecdsa.PrivateKey: + signature, err = key.Sign(rand.Reader, d, h) + //case ed25519.PrivateKey: requires go 1.13 + // signature, err = key.Sign(rand.Reader, msg, crypto.Hash(0)) + default: + return fmt.Errorf("Unsupported private key") + } + if err != nil { + return err + } + + sb.Reset() + sb.WriteString("(request-target) date host digest") + for _, h := range auth.SignedHeaders { + sb.WriteRune(' ') + sb.WriteString(strings.ToLower(h)) + } + r.Header.Set("Host", r.Host) + r.Header.Set("Date", date) + r.Header.Set("Digest", digest) + authStr := fmt.Sprintf(`Signature keyId="%s", algorithm="%s", headers="%s", signature="%s"`, + auth.KeyId, auth.Algorithm, sb.String(), base64.StdEncoding.EncodeToString(signature)) + r.Header.Set("Authorization", authStr) + return nil +} + // prepareRequest build the request func (c *APIClient) prepareRequest( ctx context.Context, @@ -363,6 +459,16 @@ func (c *APIClient) prepareRequest( localVarRequest.Header.Add(header, value) } + if ctx != nil { + // HTTP Signature Authentication. All request headers must be set (including default headers) + // because the headers may be included in the signature. + if auth, ok := ctx.Value(ContextHttpSignatureAuth).(HttpSignatureAuth); ok { + err = c.signRequest(ctx, localVarRequest, auth) + if err != nil { + return nil, err + } + } + } return localVarRequest, nil } diff --git a/samples/client/petstore/go-experimental/go-petstore/configuration.go b/samples/client/petstore/go-experimental/go-petstore/configuration.go index 153d3a46ef3a..0c01eae22005 100644 --- a/samples/client/petstore/go-experimental/go-petstore/configuration.go +++ b/samples/client/petstore/go-experimental/go-petstore/configuration.go @@ -11,8 +11,13 @@ package petstore import ( "context" + "crypto" + "crypto/x509" + "encoding/pem" "fmt" + "io/ioutil" "net/http" + "os" "strings" ) @@ -39,6 +44,9 @@ var ( // ContextAPIKeys takes a string apikey as authentication for the request ContextAPIKeys = contextKey("apiKeys") + // ContextHttpSignatureAuth takes HttpSignatureAuth as authentication for the request. + ContextHttpSignatureAuth = contextKey("httpsignature") + // ContextServerIndex uses a server configuration from the index. ContextServerIndex = contextKey("serverIndex") @@ -64,6 +72,45 @@ type APIKey struct { Prefix string } +// HttpSignatureAuth provides http message signature authentication to a request passed via context using ContextHttpSignatureAuth +type HttpSignatureAuth struct { + KeyId string // A key identifier. + PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. + Algorithm string // The signature algorithm. Supported values are rsa-sha256, rsa-sha512, hs2019. + SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. +} + +// LoadPrivateKey reads the private key from the specified file. +func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { + var file *os.File + file, err = os.Open(filename) + if err != nil { + return err + } + defer func() { + err = file.Close() + }() + var priv []byte + priv, err = ioutil.ReadAll(file) + if err != nil { + return err + } + privPem, _ := pem.Decode(priv) + switch privPem.Type { + case "RSA PRIVATE KEY": + if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privPem.Bytes); err != nil { + return err + } + case "ECDSA PRIVATE KEY": + if h.PrivateKey, err = x509.ParsePKCS8PrivateKey(privPem.Bytes); err != nil { + return err + } + default: + return fmt.Errorf("Key '%s' is not supported", privPem.Type) + } + return nil +} + // ServerVariable stores the information about a server variable type ServerVariable struct { Description string From d626293577ccd08a5b10b617498e30c8602f6eee Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Mon, 13 Jan 2020 13:56:13 -0800 Subject: [PATCH 08/48] Add support for HTTP signature --- .../org/openapitools/codegen/CodegenSecurity.java | 7 +++++-- .../org/openapitools/codegen/DefaultCodegen.java | 5 +++++ .../org/openapitools/codegen/DefaultGenerator.java | 13 +++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenSecurity.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenSecurity.java index e7abb8c90d1a..65dfe5c8e491 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenSecurity.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenSecurity.java @@ -30,7 +30,7 @@ public class CodegenSecurity { public String scheme; public Boolean hasMore, isBasic, isOAuth, isApiKey; // is Basic is true for all http authentication type. Those are to differentiate basic and bearer authentication - public Boolean isBasicBasic, isBasicBearer; + public Boolean isBasicBasic, isBasicBearer, isHttpSignature; public String bearerFormat; public Map vendorExtensions = new HashMap(); // ApiKey specific @@ -50,6 +50,7 @@ public CodegenSecurity filterByScopeNames(List filterScopes) { filteredSecurity.hasMore = false; filteredSecurity.isBasic = isBasic; filteredSecurity.isBasicBasic = isBasicBasic; + filteredSecurity.isHttpSignature = isHttpSignature; filteredSecurity.isBasicBearer = isBasicBearer; filteredSecurity.isApiKey = isApiKey; filteredSecurity.isOAuth = isOAuth; @@ -97,6 +98,7 @@ public boolean equals(Object o) { Objects.equals(isOAuth, that.isOAuth) && Objects.equals(isApiKey, that.isApiKey) && Objects.equals(isBasicBasic, that.isBasicBasic) && + Objects.equals(isHttpSignature, that.isHttpSignature) && Objects.equals(isBasicBearer, that.isBasicBearer) && Objects.equals(bearerFormat, that.bearerFormat) && Objects.equals(vendorExtensions, that.vendorExtensions) && @@ -117,7 +119,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(name, type, scheme, hasMore, isBasic, isOAuth, isApiKey, isBasicBasic, isBasicBearer, + return Objects.hash(name, type, scheme, hasMore, isBasic, isOAuth, isApiKey, isBasicBasic, isHttpSignature, isBasicBearer, bearerFormat, vendorExtensions, keyParamName, isKeyInQuery, isKeyInHeader, isKeyInCookie, flow, authorizationUrl, tokenUrl, scopes, isCode, isPassword, isApplication, isImplicit); } @@ -133,6 +135,7 @@ public String toString() { sb.append(", isOAuth=").append(isOAuth); sb.append(", isApiKey=").append(isApiKey); sb.append(", isBasicBasic=").append(isBasicBasic); + sb.append(", isHttpSignature=").append(isHttpSignature); sb.append(", isBasicBearer=").append(isBasicBearer); sb.append(", bearerFormat='").append(bearerFormat).append('\''); sb.append(", vendorExtensions=").append(vendorExtensions); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index d157cb808e0b..528ee088232b 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -3605,6 +3605,7 @@ public List fromSecurity(Map securitySc cs.name = key; cs.type = securityScheme.getType().toString(); cs.isCode = cs.isPassword = cs.isApplication = cs.isImplicit = false; + cs.isHttpSignature = false; cs.isBasicBasic = cs.isBasicBearer = false; cs.scheme = securityScheme.getScheme(); if (securityScheme.getExtensions() != null) { @@ -3626,6 +3627,10 @@ public List fromSecurity(Map securitySc } else if ("bearer".equals(securityScheme.getScheme())) { cs.isBasicBearer = true; cs.bearerFormat = securityScheme.getBearerFormat(); + } else if ("signature".equals(securityScheme.getScheme())) { + cs.isHttpSignature = true; + } else { + throw new RuntimeException("Unsupported security scheme: " + securityScheme.getScheme()); } } else if (SecurityScheme.Type.OAUTH2.equals(securityScheme.getType())) { cs.isKeyInHeader = cs.isKeyInQuery = cs.isKeyInCookie = cs.isApiKey = cs.isBasic = false; diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java index 2d263ed3867b..7d3eb5dbab09 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java @@ -851,6 +851,9 @@ private Map buildSupportFileBundle(List allOperations, L if (hasBearerMethods(authMethods)) { bundle.put("hasBearerMethods", true); } + if (hasHttpSignatureMethods(authMethods)) { + bundle.put("hasHttpSignatureMethods", true); + } } List servers = config.fromServers(openAPI.getServers()); @@ -1332,6 +1335,16 @@ private boolean hasBearerMethods(List authMethods) { return false; } + private boolean hasHttpSignatureMethods(List authMethods) { + for (CodegenSecurity cs : authMethods) { + if (Boolean.TRUE.equals(cs.isHttpSignature)) { + return true; + } + } + + return false; + } + private List getOAuthMethods(List authMethods) { List oauthMethods = new ArrayList<>(); From 9eca52c65f9ba6c2c7ab0a96aa0e3c64c4600874 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Mon, 13 Jan 2020 14:02:50 -0800 Subject: [PATCH 09/48] Add code comments --- .../main/java/org/openapitools/codegen/CodegenSecurity.java | 4 +++- .../main/java/org/openapitools/codegen/DefaultCodegen.java | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenSecurity.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenSecurity.java index 65dfe5c8e491..62b456803746 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenSecurity.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenSecurity.java @@ -29,7 +29,9 @@ public class CodegenSecurity { public String type; public String scheme; public Boolean hasMore, isBasic, isOAuth, isApiKey; - // is Basic is true for all http authentication type. Those are to differentiate basic and bearer authentication + // is Basic is true for all http authentication type. + // Those are to differentiate basic and bearer authentication + // isHttpSignature is to support https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ public Boolean isBasicBasic, isBasicBearer, isHttpSignature; public String bearerFormat; public Map vendorExtensions = new HashMap(); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 528ee088232b..cd48d0816723 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -3628,6 +3628,9 @@ public List fromSecurity(Map securitySc cs.isBasicBearer = true; cs.bearerFormat = securityScheme.getBearerFormat(); } else if ("signature".equals(securityScheme.getScheme())) { + // HTTP signature as defined in https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ + // The registry of security schemes is maintained by IANA. + // https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml cs.isHttpSignature = true; } else { throw new RuntimeException("Unsupported security scheme: " + securityScheme.getScheme()); From a59f75979a851cbbdd1a8c818bf70adb28c691d3 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Mon, 13 Jan 2020 14:08:20 -0800 Subject: [PATCH 10/48] Add code comments --- .../main/java/org/openapitools/codegen/DefaultGenerator.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java index 7d3eb5dbab09..161a136b7e06 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java @@ -1335,6 +1335,9 @@ private boolean hasBearerMethods(List authMethods) { return false; } + // hasHttpSignatureMethods returns true if the specified OAS model has + // HTTP signature methods. + // The HTTP signature scheme is defined in https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ private boolean hasHttpSignatureMethods(List authMethods) { for (CodegenSecurity cs : authMethods) { if (Boolean.TRUE.equals(cs.isHttpSignature)) { From 9e6395fa4ec299a16e18ff895763177a21e3eb1b Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Mon, 13 Jan 2020 16:24:03 -0800 Subject: [PATCH 11/48] add code comments --- .../src/main/java/org/openapitools/codegen/DefaultCodegen.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index cd48d0816723..157ec4a0a80d 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -3631,6 +3631,8 @@ public List fromSecurity(Map securitySc // HTTP signature as defined in https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ // The registry of security schemes is maintained by IANA. // https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml + // As of January 2020, the "signature" scheme has not been registered with IANA yet. + // This scheme may have to be changed when it is officially registered with IANA. cs.isHttpSignature = true; } else { throw new RuntimeException("Unsupported security scheme: " + securityScheme.getScheme()); From a6fd085e23d935cc5355f7eeeff0b870334f3881 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Mon, 13 Jan 2020 20:36:28 -0800 Subject: [PATCH 12/48] use bytes.Buffer instead of strings.Builder --- .../src/main/resources/go-experimental/client.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache index 3e2c0b3bbec8..5cb8948614a7 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache @@ -236,7 +236,7 @@ func (c *APIClient) signRequest( digest = digest + base64.StdEncoding.EncodeToString(d) // Build the string to be signed. - var sb strings.Builder + var sb bytes.Buffer fmt.Fprintf(&sb, "(request-target): %s %s", strings.ToLower(r.Method), r.URL.EscapedPath()) if r.URL.RawQuery != "" { // The ":path" pseudo-header field includes the path and query parts From 70040b7788e8a611cca1a8f32fdcee421b134d10 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Mon, 13 Jan 2020 20:37:33 -0800 Subject: [PATCH 13/48] use bytes.Buffer instead of strings.Builder --- .../go-experimental/go-petstore/client.go | 2 +- .../go-experimental/go-petstore/client.go | 106 ++++++++++++++++++ .../go-petstore/configuration.go | 47 ++++++++ 3 files changed, 154 insertions(+), 1 deletion(-) diff --git a/samples/client/petstore/go-experimental/go-petstore/client.go b/samples/client/petstore/go-experimental/go-petstore/client.go index 5e826a8627a6..07683f2b7f37 100644 --- a/samples/client/petstore/go-experimental/go-petstore/client.go +++ b/samples/client/petstore/go-experimental/go-petstore/client.go @@ -247,7 +247,7 @@ func (c *APIClient) signRequest( digest = digest + base64.StdEncoding.EncodeToString(d) // Build the string to be signed. - var sb strings.Builder + var sb bytes.Buffer fmt.Fprintf(&sb, "(request-target): %s %s", strings.ToLower(r.Method), r.URL.EscapedPath()) if r.URL.RawQuery != "" { // The ":path" pseudo-header field includes the path and query parts diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/client.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/client.go index 6b2ddc386d56..387b0e00a96a 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/client.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/client.go @@ -12,6 +12,11 @@ package openapi import ( "bytes" "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/ecdsa" + "encoding/base64" "encoding/json" "encoding/xml" "errors" @@ -206,6 +211,97 @@ func (c *APIClient) GetConfig() *Configuration { return c.cfg } +// signRequest signs the request using HTTP signature. +// See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ +func (c *APIClient) signRequest( + ctx context.Context, + r *http.Request, + auth HttpSignatureAuth) error { + + if auth.PrivateKey == nil { + return errors.New("Private key is not set") + } + date := time.Now().UTC().Format(http.TimeFormat) + var digest string + var h crypto.Hash + var err error + switch auth.Algorithm { + case "rsa-sha512", "hs2019": + h = crypto.SHA512 + digest = "SHA-512=" + case "rsa-sha256": + // This is deprecated and should not longer be used. + h = crypto.SHA256 + digest = "SHA-256=" + default: + return fmt.Errorf("Unsupported signature algorith: %v", auth.Algorithm) + } + if !h.Available() { + return fmt.Errorf("Hash '%v' is not available", h) + } + // Calculate body digest per RFC 3230 section 4.3.2 + bodyHash := h.New() + if r.Body != nil { + if _, err = io.Copy(bodyHash, r.Body); err != nil { + return err + } + } + d := bodyHash.Sum(nil) + digest = digest + base64.StdEncoding.EncodeToString(d) + + // Build the string to be signed. + var sb bytes.Buffer + fmt.Fprintf(&sb, "(request-target): %s %s", strings.ToLower(r.Method), r.URL.EscapedPath()) + if r.URL.RawQuery != "" { + // The ":path" pseudo-header field includes the path and query parts + // of the target URI (the "path-absolute" production and optionally a + // '?' character followed by the "query" production (see Sections 3.3 + // and 3.4 of [RFC3986] + fmt.Fprintf(&sb, "?%s", r.URL.RawQuery) + } + fmt.Fprintf(&sb, "\ndate: %s", date) + fmt.Fprintf(&sb, "\nhost: %s", r.Host) + fmt.Fprintf(&sb, "\ndigest: %s", digest) + for _, header := range auth.SignedHeaders { + fmt.Fprintf(&sb, "\n%s: %s", strings.ToLower(header), r.Header.Get(header)) + } + msg := []byte(sb.String()) + msgHash := h.New() + if _, err = msgHash.Write(msg); err != nil { + return err + } + d = msgHash.Sum(nil) + + var signature []byte + switch key := auth.PrivateKey.(type) { + case *rsa.PrivateKey: + signature, err = rsa.SignPKCS1v15(rand.Reader, key, h, d) + case *ecdsa.PrivateKey: + signature, err = key.Sign(rand.Reader, d, h) + //case ed25519.PrivateKey: requires go 1.13 + // signature, err = key.Sign(rand.Reader, msg, crypto.Hash(0)) + default: + return fmt.Errorf("Unsupported private key") + } + if err != nil { + return err + } + + sb.Reset() + sb.WriteString("(request-target) date host digest") + for _, h := range auth.SignedHeaders { + sb.WriteRune(' ') + sb.WriteString(strings.ToLower(h)) + } + r.Header.Set("Host", r.Host) + r.Header.Set("Date", date) + r.Header.Set("Digest", digest) + authStr := fmt.Sprintf(`Signature keyId="%s", algorithm="%s", headers="%s", signature="%s"`, + auth.KeyId, auth.Algorithm, sb.String(), base64.StdEncoding.EncodeToString(signature)) + r.Header.Set("Authorization", authStr) + return nil +} + // prepareRequest build the request func (c *APIClient) prepareRequest( ctx context.Context, @@ -366,6 +462,16 @@ func (c *APIClient) prepareRequest( localVarRequest.Header.Add(header, value) } + if ctx != nil { + // HTTP Signature Authentication. All request headers must be set (including default headers) + // because the headers may be included in the signature. + if auth, ok := ctx.Value(ContextHttpSignatureAuth).(HttpSignatureAuth); ok { + err = c.signRequest(ctx, localVarRequest, auth) + if err != nil { + return nil, err + } + } + } return localVarRequest, nil } diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/configuration.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/configuration.go index 58f47a496871..3944224f1ecd 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/configuration.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/configuration.go @@ -11,8 +11,13 @@ package openapi import ( "context" + "crypto" + "crypto/x509" + "encoding/pem" "fmt" + "io/ioutil" "net/http" + "os" "strings" ) @@ -39,6 +44,9 @@ var ( // ContextAPIKeys takes a string apikey as authentication for the request ContextAPIKeys = contextKey("apiKeys") + // ContextHttpSignatureAuth takes HttpSignatureAuth as authentication for the request. + ContextHttpSignatureAuth = contextKey("httpsignature") + // ContextServerIndex uses a server configuration from the index. ContextServerIndex = contextKey("serverIndex") @@ -64,6 +72,45 @@ type APIKey struct { Prefix string } +// HttpSignatureAuth provides http message signature authentication to a request passed via context using ContextHttpSignatureAuth +type HttpSignatureAuth struct { + KeyId string // A key identifier. + PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. + Algorithm string // The signature algorithm. Supported values are rsa-sha256, rsa-sha512, hs2019. + SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. +} + +// LoadPrivateKey reads the private key from the specified file. +func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { + var file *os.File + file, err = os.Open(filename) + if err != nil { + return err + } + defer func() { + err = file.Close() + }() + var priv []byte + priv, err = ioutil.ReadAll(file) + if err != nil { + return err + } + privPem, _ := pem.Decode(priv) + switch privPem.Type { + case "RSA PRIVATE KEY": + if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privPem.Bytes); err != nil { + return err + } + case "ECDSA PRIVATE KEY": + if h.PrivateKey, err = x509.ParsePKCS8PrivateKey(privPem.Bytes); err != nil { + return err + } + default: + return fmt.Errorf("Key '%s' is not supported", privPem.Type) + } + return nil +} + // ServerVariable stores the information about a server variable type ServerVariable struct { Description string From 2ccd0f9016e7f3d5b1f1b3d042fe47ac8fb21464 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Mon, 13 Jan 2020 20:47:21 -0800 Subject: [PATCH 14/48] Fix header of ECDSA private key --- .../src/main/resources/go-experimental/configuration.mustache | 3 ++- .../petstore/go-experimental/go-petstore/configuration.go | 3 ++- .../petstore/go-experimental/go-petstore/configuration.go | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache index 5fb2688e19bf..d5e1738e9ee1 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache @@ -93,7 +93,8 @@ func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privPem.Bytes); err != nil { return err } - case "ECDSA PRIVATE KEY": + case "EC PRIVATE KEY": + // https://tools.ietf.org/html/rfc5915 section 4. if h.PrivateKey, err = x509.ParsePKCS8PrivateKey(privPem.Bytes); err != nil { return err } diff --git a/samples/client/petstore/go-experimental/go-petstore/configuration.go b/samples/client/petstore/go-experimental/go-petstore/configuration.go index 0c01eae22005..9b06fae2ce17 100644 --- a/samples/client/petstore/go-experimental/go-petstore/configuration.go +++ b/samples/client/petstore/go-experimental/go-petstore/configuration.go @@ -101,7 +101,8 @@ func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privPem.Bytes); err != nil { return err } - case "ECDSA PRIVATE KEY": + case "EC PRIVATE KEY": + // https://tools.ietf.org/html/rfc5915 section 4. if h.PrivateKey, err = x509.ParsePKCS8PrivateKey(privPem.Bytes); err != nil { return err } diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/configuration.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/configuration.go index 3944224f1ecd..e2cbac5216cd 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/configuration.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/configuration.go @@ -101,7 +101,8 @@ func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privPem.Bytes); err != nil { return err } - case "ECDSA PRIVATE KEY": + case "EC PRIVATE KEY": + // https://tools.ietf.org/html/rfc5915 section 4. if h.PrivateKey, err = x509.ParsePKCS8PrivateKey(privPem.Bytes); err != nil { return err } From 5131602c448e581fab64037d0c6453d5f07de4ec Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Mon, 13 Jan 2020 22:04:23 -0800 Subject: [PATCH 15/48] Add unit tests --- .../petstore/go-experimental/auth_test.go | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/samples/client/petstore/go-experimental/auth_test.go b/samples/client/petstore/go-experimental/auth_test.go index b788cc1199e3..edf317aeba2a 100644 --- a/samples/client/petstore/go-experimental/auth_test.go +++ b/samples/client/petstore/go-experimental/auth_test.go @@ -2,8 +2,16 @@ package main import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "io/ioutil" "net/http" "net/http/httputil" + "os" "strings" "testing" "time" @@ -199,6 +207,148 @@ func TestAPIKeyWithPrefix(t *testing.T) { } } +// Test RSA private key as published in Appendix C 'Test Values' of +// https://www.ietf.org/id/draft-cavage-http-signatures-12.txt +const rsaTestPrivateKey string = `-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF +NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F +UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB +AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA +QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK +kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg +f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u +412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc +mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7 +kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA +gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW +G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI +7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== +-----END RSA PRIVATE KEY-----` + +func writeTestRsaPemKey(t *testing.T, fileName string) { + err := ioutil.WriteFile(fileName, []byte(rsaTestPrivateKey), 0644) + if err != nil { + t.Fatalf("Error writing private key: %v", err) + } +} + +func writeRandomTestRsaPemKey(t *testing.T, fileName string) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("Error generating RSA private key file: %v", err) + } + var outFile *os.File + outFile, err = os.Create(fileName) + if err != nil { + t.Fatalf("Error creating RSA private key file: %v", err) + } + defer outFile.Close() + + var privateKey = &pem.Block{ + Type: "PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + } + + err = pem.Encode(outFile, privateKey) + if err != nil { + t.Fatalf("Error encoding RSA private key: %v", err) + } +} + +func writeRandomTestEcdsaPemKey(t *testing.T, fileName string) { + key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + t.Fatalf("Error generating ECDSA private key file: %v", err) + } + var outFile *os.File + outFile, err = os.Create(fileName) + if err != nil { + t.Fatalf("Error creating ECDSA private key file: %v", err) + } + defer outFile.Close() + + var keybytes []byte + keybytes, err = x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatalf("Error marshaling ECDSA private key: %v", err) + } + var privateKey = &pem.Block{ + Type: "PRIVATE KEY", + Bytes: keybytes, + } + + err = pem.Encode(outFile, privateKey) + if err != nil { + t.Fatalf("Error encoding ECDSA private key: %v", err) + } +} + +func TestHttpSignaturePrivateKeys(t *testing.T) { + privateKeyPath := "privatekey.pem" + { + authConfig := sw.HttpSignatureAuth{ + KeyId: "my-key-id", + Algorithm: "hs2019", + SignedHeaders: []string{"Content-Type"}, + } + // Generate test private key. + writeRandomTestRsaPemKey(t, privateKeyPath) + err := authConfig.LoadPrivateKey(privateKeyPath) + if err != nil { + t.Fatalf("Error loading private key: %v", err) + } + } + { + authConfig := sw.HttpSignatureAuth{ + KeyId: "my-key-id", + Algorithm: "hs2019", + SignedHeaders: []string{"Content-Type"}, + } + // Generate test private key. + writeRandomTestEcdsaPemKey(t, privateKeyPath) + err := authConfig.LoadPrivateKey(privateKeyPath) + if err != nil { + t.Fatalf("Error loading private key: %v", err) + } + } +} +func TestHttpSignatureAuth(t *testing.T) { + privateKeyPath := "rsa.pem" + authConfig := sw.HttpSignatureAuth{ + KeyId: "my-key-id", + Algorithm: "hs2019", + SignedHeaders: []string{"Content-Type"}, + } + writeTestRsaPemKey(t, privateKeyPath) + err := authConfig.LoadPrivateKey(privateKeyPath) + if err != nil { + t.Fatalf("Error loading private key: %v", err) + } + auth := context.WithValue(context.Background(), sw.ContextHttpSignatureAuth, authConfig) + newPet := (sw.Pet{Id: sw.PtrInt64(12992), Name: "gopher", + PhotoUrls: []string{"http://1.com", "http://2.com"}, Status: sw.PtrString("pending"), + Tags: &[]sw.Tag{sw.Tag{Id: sw.PtrInt64(1), Name: sw.PtrString("tag2")}}}) + + r, err2 := client.PetApi.AddPet(nil, newPet) + + if err2 != nil { + t.Fatalf("Error while adding pet: %v", err2) + } + if r.StatusCode != 200 { + t.Log(r) + } + + _, r, err = client.PetApi.GetPetById(auth, 12992) + if err != nil { + t.Fatalf("Error while deleting pet by id: %v", err) + } + + reqb, _ := httputil.DumpRequest(r.Request, true) + if !strings.Contains((string)(reqb), "Api_key: Bearer TEST123") { + t.Errorf("APIKey Authentication is missing") + } +} + func TestDefaultHeader(t *testing.T) { newPet := (sw.Pet{Id: sw.PtrInt64(12992), Name: "gopher", PhotoUrls: []string{"http://1.com", "http://2.com"}, Status: sw.PtrString("pending"), From af2e7a45c9162e273bb29ba72995d58e6f736a50 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Tue, 14 Jan 2020 08:14:39 -0800 Subject: [PATCH 16/48] Add unit tests, compliance with HTTP signature draft version 12 --- .../resources/go-experimental/client.mustache | 102 +++++++---- .../go-experimental/configuration.mustache | 39 +++- .../petstore/go-experimental/auth_test.go | 172 +++++++++++++++--- .../go-experimental/go-petstore/client.go | 102 +++++++---- .../go-petstore/configuration.go | 39 +++- 5 files changed, 354 insertions(+), 100 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache index 5cb8948614a7..5a8cc582a989 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache @@ -197,9 +197,9 @@ func (c *APIClient) GetConfig() *Configuration { return c.cfg } -// signRequest signs the request using HTTP signature. +// SignRequest signs the request using HTTP signature. // See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ -func (c *APIClient) signRequest( +func (c *APIClient) SignRequest( ctx context.Context, r *http.Request, auth HttpSignatureAuth) error { @@ -207,37 +207,35 @@ func (c *APIClient) signRequest( if auth.PrivateKey == nil { return errors.New("Private key is not set") } - date := time.Now().UTC().Format(http.TimeFormat) - var digest string + now := time.Now() + date := now.UTC().Format(http.TimeFormat) + // The `created` field expresses when the signature was created. + // The value MUST be a Unix timestamp integer value. See 'HTTP signature' section 2.1.4. + created := now.Unix() + var h crypto.Hash var err error + var prefix string + + // Determine the cryptographic hash to be used for the signature and the body digest. switch auth.Algorithm { - case "rsa-sha512", "hs2019": + case HttpSignatureAlgorithmRsaSha512, HttpSignatureAlgorithmHs2019: h = crypto.SHA512 - digest = "SHA-512=" - case "rsa-sha256": - // This is deprecated and should not longer be used. + prefix = "SHA-512=" + case HttpSignatureAlgorithmRsaSha256: + // This is deprecated and should no longer be used. h = crypto.SHA256 - digest = "SHA-256=" + prefix = "SHA-256=" default: return fmt.Errorf("Unsupported signature algorith: %v", auth.Algorithm) } if !h.Available() { return fmt.Errorf("Hash '%v' is not available", h) } - // Calculate body digest per RFC 3230 section 4.3.2 - bodyHash := h.New() - if r.Body != nil { - if _, err = io.Copy(bodyHash, r.Body); err != nil { - return err - } - } - d := bodyHash.Sum(nil) - digest = digest + base64.StdEncoding.EncodeToString(d) - // Build the string to be signed. + // Build the "(request-target)" signature header. var sb bytes.Buffer - fmt.Fprintf(&sb, "(request-target): %s %s", strings.ToLower(r.Method), r.URL.EscapedPath()) + fmt.Fprintf(&sb, "%s %s", strings.ToLower(r.Method), r.URL.EscapedPath()) if r.URL.RawQuery != "" { // The ":path" pseudo-header field includes the path and query parts // of the target URI (the "path-absolute" production and optionally a @@ -245,18 +243,54 @@ func (c *APIClient) signRequest( // and 3.4 of [RFC3986] fmt.Fprintf(&sb, "?%s", r.URL.RawQuery) } - fmt.Fprintf(&sb, "\ndate: %s", date) - fmt.Fprintf(&sb, "\nhost: %s", r.Host) - fmt.Fprintf(&sb, "\ndigest: %s", digest) - for _, header := range auth.SignedHeaders { - fmt.Fprintf(&sb, "\n%s: %s", strings.ToLower(header), r.Header.Get(header)) + requestTarget := sb.String() + sb.Reset() + + // Build the string to be signed. + signedHeaders := auth.SignedHeaders + if len(signedHeaders) == 0 { + signedHeaders = []string{HttpSignatureParameterCreated} + } + for i, header := range signedHeaders { + header = strings.ToLower(header) + var value string + switch header { + case HttpSignatureParameterRequestTarget: + value = requestTarget + case HttpSignatureParameterCreated: + value = fmt.Sprintf("%d", created) + case "date": + value = date + r.Header.Set("Date", date) + case "digest": + // Calculate the digest of the HTTP request body. + // Calculate body digest per RFC 3230 section 4.3.2 + bodyHash := h.New() + if r.Body != nil { + if _, err = io.Copy(bodyHash, r.Body); err != nil { + return err + } + } + d := bodyHash.Sum(nil) + value = prefix + base64.StdEncoding.EncodeToString(d) + r.Header.Set("Digest", value) + case "host": + value = r.Host + r.Header.Set("Host", r.Host) + default: + value = r.Header.Get(header) + } + if i > 0 { + fmt.Fprintf(&sb, "\n") + } + fmt.Fprintf(&sb, "%s: %s", header, value) } msg := []byte(sb.String()) msgHash := h.New() if _, err = msgHash.Write(msg); err != nil { return err } - d = msgHash.Sum(nil) + d := msgHash.Sum(nil) var signature []byte switch key := auth.PrivateKey.(type) { @@ -274,14 +308,12 @@ func (c *APIClient) signRequest( } sb.Reset() - sb.WriteString("(request-target) date host digest") - for _, h := range auth.SignedHeaders { - sb.WriteRune(' ') - sb.WriteString(strings.ToLower(h)) - } - r.Header.Set("Host", r.Host) - r.Header.Set("Date", date) - r.Header.Set("Digest", digest) + for i, header := range signedHeaders { + if i > 0 { + sb.WriteRune(' ') + } + sb.WriteString(strings.ToLower(header)) + } authStr := fmt.Sprintf(`Signature keyId="%s", algorithm="%s", headers="%s", signature="%s"`, auth.KeyId, auth.Algorithm, sb.String(), base64.StdEncoding.EncodeToString(signature)) r.Header.Set("Authorization", authStr) @@ -452,7 +484,7 @@ func (c *APIClient) prepareRequest( // HTTP Signature Authentication. All request headers must be set (including default headers) // because the headers may be included in the signature. if auth, ok := ctx.Value(ContextHttpSignatureAuth).(HttpSignatureAuth); ok { - err = c.signRequest(ctx, localVarRequest, auth) + err = c.SignRequest(ctx, localVarRequest, auth) if err != nil { return nil, err } diff --git a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache index d5e1738e9ee1..24ca4e1aa7a9 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache @@ -64,11 +64,46 @@ type APIKey struct { Prefix string } -// HttpSignatureAuth provides http message signature authentication to a request passed via context using ContextHttpSignatureAuth +const ( + // Constants for HTTP signature parameters. + // The '(request-target)' parameter concatenates the lowercased :method, an + // ASCII space, and the :path pseudo-headers. + HttpSignatureParameterRequestTarget string = "(request-target)" + // The '(created)' parameter expresses when the signature was + // created. The value MUST be a Unix timestamp integer value. + HttpSignatureParameterCreated string = "(created)" + // The '(expires)' parameter expresses when the signature ceases to + // be valid. The value MUST be a Unix timestamp integer value. + HttpSignatureParameterExpires string = "(expires)" + + HttpSignatureAlgorithmHs2019 string = "hs2019" + HttpSignatureAlgorithmRsaSha512 string = "rsa-sha512" // deprecated + HttpSignatureAlgorithmRsaSha256 string = "rsa-sha256" // deprecated +) + +// HttpSignatureAuth provides http message signature authentication to a request passed +// via context using ContextHttpSignatureAuth. +// SignedHeaders specifies the list of HTTP headers that are included when generating +// the message signature. +// The two special signature headers '(request-target)' and '(created)' SHOULD be +// included in SignedHeaders. +// The '(created)' header expresses when the signature was created. +// The '(request-target)' header is a concatenation of the lowercased :method, an +// ASCII space, and the :path pseudo-headers. +// +// For example, SignedHeaders can be set to: +// (request-target) (created) date host digest +// +// When SignedHeaders is not specified, the client defaults to a single value, '(created)', +// in the list of HTTP headers. +// When SignedHeaders contains the 'Digest' value, the client performs the following operations: +// 1. Calculate a digest of request body, as specified in RFC3230, section 4.3.2. +// 2. Set the 'Digest' header in the request body. +// 3. Include the 'Digest' header and value in the HTTP signature. type HttpSignatureAuth struct { KeyId string // A key identifier. PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. - Algorithm string // The signature algorithm. Supported values are rsa-sha256, rsa-sha512, hs2019. + Algorithm string // The signature algorithm. Supported value is 'hs2019'. SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. } diff --git a/samples/client/petstore/go-experimental/auth_test.go b/samples/client/petstore/go-experimental/auth_test.go index edf317aeba2a..dd3d879a96f6 100644 --- a/samples/client/petstore/go-experimental/auth_test.go +++ b/samples/client/petstore/go-experimental/auth_test.go @@ -1,13 +1,13 @@ package main import ( + "bytes" "context" - "crypto/ecdsa" - "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" + "fmt" "io/ioutil" "net/http" "net/http/httputil" @@ -255,6 +255,9 @@ func writeRandomTestRsaPemKey(t *testing.T, fileName string) { } } +/* +Commented out because OpenAPITools is configured to use golang 1.8 at build time +x509.MarshalPKCS8PrivateKey is not present. func writeRandomTestEcdsaPemKey(t *testing.T, fileName string) { key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) if err != nil { @@ -282,6 +285,7 @@ func writeRandomTestEcdsaPemKey(t *testing.T, fileName string) { t.Fatalf("Error encoding ECDSA private key: %v", err) } } +*/ func TestHttpSignaturePrivateKeys(t *testing.T) { privateKeyPath := "privatekey.pem" @@ -298,36 +302,34 @@ func TestHttpSignaturePrivateKeys(t *testing.T) { t.Fatalf("Error loading private key: %v", err) } } - { - authConfig := sw.HttpSignatureAuth{ - KeyId: "my-key-id", - Algorithm: "hs2019", - SignedHeaders: []string{"Content-Type"}, - } - // Generate test private key. - writeRandomTestEcdsaPemKey(t, privateKeyPath) - err := authConfig.LoadPrivateKey(privateKeyPath) - if err != nil { - t.Fatalf("Error loading private key: %v", err) + /* + { + authConfig := sw.HttpSignatureAuth{ + KeyId: "my-key-id", + Algorithm: "hs2019", + SignedHeaders: []string{"Content-Type"}, + } + // Generate test private key. + writeRandomTestEcdsaPemKey(t, privateKeyPath) + err := authConfig.LoadPrivateKey(privateKeyPath) + if err != nil { + t.Fatalf("Error loading private key: %v", err) + } } - } + */ } -func TestHttpSignatureAuth(t *testing.T) { +func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth) string { privateKeyPath := "rsa.pem" - authConfig := sw.HttpSignatureAuth{ - KeyId: "my-key-id", - Algorithm: "hs2019", - SignedHeaders: []string{"Content-Type"}, - } writeTestRsaPemKey(t, privateKeyPath) err := authConfig.LoadPrivateKey(privateKeyPath) if err != nil { t.Fatalf("Error loading private key: %v", err) } - auth := context.WithValue(context.Background(), sw.ContextHttpSignatureAuth, authConfig) + auth := context.WithValue(context.Background(), sw.ContextHttpSignatureAuth, *authConfig) newPet := (sw.Pet{Id: sw.PtrInt64(12992), Name: "gopher", - PhotoUrls: []string{"http://1.com", "http://2.com"}, Status: sw.PtrString("pending"), - Tags: &[]sw.Tag{sw.Tag{Id: sw.PtrInt64(1), Name: sw.PtrString("tag2")}}}) + PhotoUrls: []string{"http://1.com", "http://2.com"}, + Status: sw.PtrString("pending"), + Tags: &[]sw.Tag{sw.Tag{Id: sw.PtrInt64(1), Name: sw.PtrString("tag2")}}}) r, err2 := client.PetApi.AddPet(nil, newPet) @@ -342,10 +344,128 @@ func TestHttpSignatureAuth(t *testing.T) { if err != nil { t.Fatalf("Error while deleting pet by id: %v", err) } - + // The request should look like this: + // + // GET /v2/pet/12992 HTTP/1.1 + // Host: petstore.swagger.io:80 + // Accept: application/json + // Authorization: Signature keyId="my-key-id", algorithm="hs2019", headers="(request-target) date host digest content-type", signature="RMJZjVVxzlH02wlxiQgUYDe4QxZaI5IJNIfB2BK8Dhbv3WQ2gw0xyqC+5HiKUmT/cfchhhkUNNsUtiVRnjZmFwtSfYxHfiQvH3KWXlLCMwKGNQC3YzD9lnoWdx0pA5Kxbr0/ygmr3+lTyuN2PJg4IS7Ji/AaKAqIZx7RsHS8Bxw=" + // Date: Tue, 14 Jan 2020 06:41:22 GMT + // Digest: SHA-512=z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg== + // Testheader: testvalue + // User-Agent: OpenAPI-Generator/1.0.0/go reqb, _ := httputil.DumpRequest(r.Request, true) - if !strings.Contains((string)(reqb), "Api_key: Bearer TEST123") { - t.Errorf("APIKey Authentication is missing") + reqt := (string)(reqb) + fmt.Printf("Request with HTTP signature:\n%v\n", reqt) + var sb bytes.Buffer + fmt.Fprintf(&sb, `Authorization: Signature keyId="%s", algorithm="%s", headers="`, + authConfig.KeyId, authConfig.Algorithm) + for i, header := range authConfig.SignedHeaders { + header = strings.ToLower(header) + if i > 0 { + fmt.Fprintf(&sb, " ") + } + fmt.Fprintf(&sb, header) + switch header { + case "date": + if !strings.Contains(reqt, "Date: ") { + t.Errorf("Date header is incorrect") + } + case "digest": + var prefix string + switch authConfig.Algorithm { + case sw.HttpSignatureAlgorithmRsaSha256: + prefix = "SHA-256=" + default: + prefix = "SHA-512=" + } + if !strings.Contains(reqt, fmt.Sprintf("Digest: %s", prefix)) { + t.Errorf("Digest header is incorrect") + } + } + } + if len(authConfig.SignedHeaders) == 0 { + fmt.Fprintf(&sb, sw.HttpSignatureParameterCreated) + } + fmt.Fprintf(&sb, `", signature="`) + if !strings.Contains(reqt, sb.String()) { + t.Errorf("Authorization header is incorrect") + } + return r.Request.Header.Get("Authorization") +} +func TestHttpSignatureAuth(t *testing.T) { + authConfig := sw.HttpSignatureAuth{ + KeyId: "my-key-id", + Algorithm: sw.HttpSignatureAlgorithmHs2019, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Date", // The date and time at which the message was originated. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + } + executeHttpSignatureAuth(t, &authConfig) + + // Test with empty signed headers. The client should automatically add the "(created)" parameter by default. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + Algorithm: sw.HttpSignatureAlgorithmHs2019, + SignedHeaders: []string{}, + } + executeHttpSignatureAuth(t, &authConfig) + + // Test with deprecated RSA-SHA512, some servers may still support it. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + Algorithm: sw.HttpSignatureAlgorithmRsaSha512, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Date", // The date and time at which the message was originated. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + } + executeHttpSignatureAuth(t, &authConfig) + + // Test with deprecated RSA-SHA256, some servers may still support it. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + Algorithm: sw.HttpSignatureAlgorithmRsaSha256, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Date", // The date and time at which the message was originated. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + } + executeHttpSignatureAuth(t, &authConfig) + + // Test with headers without date. This makes it possible to get a fixed signature, used for unit test purpose. + // This should not be used in production code as it could potentially allow replay attacks. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + Algorithm: sw.HttpSignatureAlgorithmHs2019, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + } + authorizationHeaderValue := executeHttpSignatureAuth(t, &authConfig) + expectedSignature := "ED7P+ETthVNx45FZ6Z1lzNDuIE+QuR05GpNJCg7yVGJGDng98inMiPumgdUiljZZr3aEacHXH2Ln8epF4Op6GRGrNYAjXVwK2Nm/6zxt1EEr2JAfA/L5eNjJq9VfZzPjcdanjfqEKvrY6isPpQLsBwRlazhbITsW5E6hAsbVnGk=" + expectedAuthorizationHeader := fmt.Sprintf( + `Signature keyId="my-key-id", `+ + `algorithm="hs2019", headers="(request-target) host content-type digest", `+ + `signature="%s"`, expectedSignature) + if authorizationHeaderValue != expectedAuthorizationHeader { + t.Errorf("Authorization header value is incorrect. Got '%s' but expected '%s'", authorizationHeaderValue, expectedAuthorizationHeader) } } diff --git a/samples/client/petstore/go-experimental/go-petstore/client.go b/samples/client/petstore/go-experimental/go-petstore/client.go index 07683f2b7f37..f6ce8cdcbcac 100644 --- a/samples/client/petstore/go-experimental/go-petstore/client.go +++ b/samples/client/petstore/go-experimental/go-petstore/client.go @@ -208,9 +208,9 @@ func (c *APIClient) GetConfig() *Configuration { return c.cfg } -// signRequest signs the request using HTTP signature. +// SignRequest signs the request using HTTP signature. // See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ -func (c *APIClient) signRequest( +func (c *APIClient) SignRequest( ctx context.Context, r *http.Request, auth HttpSignatureAuth) error { @@ -218,37 +218,35 @@ func (c *APIClient) signRequest( if auth.PrivateKey == nil { return errors.New("Private key is not set") } - date := time.Now().UTC().Format(http.TimeFormat) - var digest string + now := time.Now() + date := now.UTC().Format(http.TimeFormat) + // The `created` field expresses when the signature was created. + // The value MUST be a Unix timestamp integer value. See 'HTTP signature' section 2.1.4. + created := now.Unix() + var h crypto.Hash var err error + var prefix string + + // Determine the cryptographic hash to be used for the signature and the body digest. switch auth.Algorithm { - case "rsa-sha512", "hs2019": + case HttpSignatureAlgorithmRsaSha512, HttpSignatureAlgorithmHs2019: h = crypto.SHA512 - digest = "SHA-512=" - case "rsa-sha256": - // This is deprecated and should not longer be used. + prefix = "SHA-512=" + case HttpSignatureAlgorithmRsaSha256: + // This is deprecated and should no longer be used. h = crypto.SHA256 - digest = "SHA-256=" + prefix = "SHA-256=" default: return fmt.Errorf("Unsupported signature algorith: %v", auth.Algorithm) } if !h.Available() { return fmt.Errorf("Hash '%v' is not available", h) } - // Calculate body digest per RFC 3230 section 4.3.2 - bodyHash := h.New() - if r.Body != nil { - if _, err = io.Copy(bodyHash, r.Body); err != nil { - return err - } - } - d := bodyHash.Sum(nil) - digest = digest + base64.StdEncoding.EncodeToString(d) - // Build the string to be signed. + // Build the "(request-target)" signature header. var sb bytes.Buffer - fmt.Fprintf(&sb, "(request-target): %s %s", strings.ToLower(r.Method), r.URL.EscapedPath()) + fmt.Fprintf(&sb, "%s %s", strings.ToLower(r.Method), r.URL.EscapedPath()) if r.URL.RawQuery != "" { // The ":path" pseudo-header field includes the path and query parts // of the target URI (the "path-absolute" production and optionally a @@ -256,18 +254,54 @@ func (c *APIClient) signRequest( // and 3.4 of [RFC3986] fmt.Fprintf(&sb, "?%s", r.URL.RawQuery) } - fmt.Fprintf(&sb, "\ndate: %s", date) - fmt.Fprintf(&sb, "\nhost: %s", r.Host) - fmt.Fprintf(&sb, "\ndigest: %s", digest) - for _, header := range auth.SignedHeaders { - fmt.Fprintf(&sb, "\n%s: %s", strings.ToLower(header), r.Header.Get(header)) + requestTarget := sb.String() + sb.Reset() + + // Build the string to be signed. + signedHeaders := auth.SignedHeaders + if len(signedHeaders) == 0 { + signedHeaders = []string{HttpSignatureParameterCreated} + } + for i, header := range signedHeaders { + header = strings.ToLower(header) + var value string + switch header { + case HttpSignatureParameterRequestTarget: + value = requestTarget + case HttpSignatureParameterCreated: + value = fmt.Sprintf("%d", created) + case "date": + value = date + r.Header.Set("Date", date) + case "digest": + // Calculate the digest of the HTTP request body. + // Calculate body digest per RFC 3230 section 4.3.2 + bodyHash := h.New() + if r.Body != nil { + if _, err = io.Copy(bodyHash, r.Body); err != nil { + return err + } + } + d := bodyHash.Sum(nil) + value = prefix + base64.StdEncoding.EncodeToString(d) + r.Header.Set("Digest", value) + case "host": + value = r.Host + r.Header.Set("Host", r.Host) + default: + value = r.Header.Get(header) + } + if i > 0 { + fmt.Fprintf(&sb, "\n") + } + fmt.Fprintf(&sb, "%s: %s", header, value) } msg := []byte(sb.String()) msgHash := h.New() if _, err = msgHash.Write(msg); err != nil { return err } - d = msgHash.Sum(nil) + d := msgHash.Sum(nil) var signature []byte switch key := auth.PrivateKey.(type) { @@ -285,14 +319,12 @@ func (c *APIClient) signRequest( } sb.Reset() - sb.WriteString("(request-target) date host digest") - for _, h := range auth.SignedHeaders { - sb.WriteRune(' ') - sb.WriteString(strings.ToLower(h)) - } - r.Header.Set("Host", r.Host) - r.Header.Set("Date", date) - r.Header.Set("Digest", digest) + for i, header := range signedHeaders { + if i > 0 { + sb.WriteRune(' ') + } + sb.WriteString(strings.ToLower(header)) + } authStr := fmt.Sprintf(`Signature keyId="%s", algorithm="%s", headers="%s", signature="%s"`, auth.KeyId, auth.Algorithm, sb.String(), base64.StdEncoding.EncodeToString(signature)) r.Header.Set("Authorization", authStr) @@ -463,7 +495,7 @@ func (c *APIClient) prepareRequest( // HTTP Signature Authentication. All request headers must be set (including default headers) // because the headers may be included in the signature. if auth, ok := ctx.Value(ContextHttpSignatureAuth).(HttpSignatureAuth); ok { - err = c.signRequest(ctx, localVarRequest, auth) + err = c.SignRequest(ctx, localVarRequest, auth) if err != nil { return nil, err } diff --git a/samples/client/petstore/go-experimental/go-petstore/configuration.go b/samples/client/petstore/go-experimental/go-petstore/configuration.go index 9b06fae2ce17..4fb4abacf773 100644 --- a/samples/client/petstore/go-experimental/go-petstore/configuration.go +++ b/samples/client/petstore/go-experimental/go-petstore/configuration.go @@ -72,11 +72,46 @@ type APIKey struct { Prefix string } -// HttpSignatureAuth provides http message signature authentication to a request passed via context using ContextHttpSignatureAuth +const ( + // Constants for HTTP signature parameters. + // The '(request-target)' parameter concatenates the lowercased :method, an + // ASCII space, and the :path pseudo-headers. + HttpSignatureParameterRequestTarget string = "(request-target)" + // The '(created)' parameter expresses when the signature was + // created. The value MUST be a Unix timestamp integer value. + HttpSignatureParameterCreated string = "(created)" + // The '(expires)' parameter expresses when the signature ceases to + // be valid. The value MUST be a Unix timestamp integer value. + HttpSignatureParameterExpires string = "(expires)" + + HttpSignatureAlgorithmHs2019 string = "hs2019" + HttpSignatureAlgorithmRsaSha512 string = "rsa-sha512" // deprecated + HttpSignatureAlgorithmRsaSha256 string = "rsa-sha256" // deprecated +) + +// HttpSignatureAuth provides http message signature authentication to a request passed +// via context using ContextHttpSignatureAuth. +// SignedHeaders specifies the list of HTTP headers that are included when generating +// the message signature. +// The two special signature headers '(request-target)' and '(created)' SHOULD be +// included in SignedHeaders. +// The '(created)' header expresses when the signature was created. +// The '(request-target)' header is a concatenation of the lowercased :method, an +// ASCII space, and the :path pseudo-headers. +// +// For example, SignedHeaders can be set to: +// (request-target) (created) date host digest +// +// When SignedHeaders is not specified, the client defaults to a single value, '(created)', +// in the list of HTTP headers. +// When SignedHeaders contains the 'Digest' value, the client performs the following operations: +// 1. Calculate a digest of request body, as specified in RFC3230, section 4.3.2. +// 2. Set the 'Digest' header in the request body. +// 3. Include the 'Digest' header and value in the HTTP signature. type HttpSignatureAuth struct { KeyId string // A key identifier. PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. - Algorithm string // The signature algorithm. Supported values are rsa-sha256, rsa-sha512, hs2019. + Algorithm string // The signature algorithm. Supported value is 'hs2019'. SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. } From 24944ca862967e46fae1f3a41e0c1d25caf80622 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Tue, 14 Jan 2020 08:33:43 -0800 Subject: [PATCH 17/48] improve code comments --- .../main/resources/go-experimental/configuration.mustache | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache index 24ca4e1aa7a9..1111081e6155 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache @@ -81,8 +81,12 @@ const ( HttpSignatureAlgorithmRsaSha256 string = "rsa-sha256" // deprecated ) -// HttpSignatureAuth provides http message signature authentication to a request passed +// HttpSignatureAuth provides HTTP signature authentication to a request passed // via context using ContextHttpSignatureAuth. +// An 'Authorization' header is calculated by creating a hash of select headers, +// and optionally the body of the HTTP request, then signing the hash value using +// a private key which is available to the client. +// // SignedHeaders specifies the list of HTTP headers that are included when generating // the message signature. // The two special signature headers '(request-target)' and '(created)' SHOULD be From cc564d37fa43f8c7f0a1c8d27179490215b1ac47 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Tue, 14 Jan 2020 09:12:05 -0800 Subject: [PATCH 18/48] Add unit tests, compliance with HTTP signature draft version 12 --- .../resources/go-experimental/client.mustache | 19 ++- .../go-experimental/configuration.mustache | 19 +-- .../petstore/go-experimental/auth_test.go | 56 ++++++--- .../go-petstore/api/openapi.yaml | 112 +++++++++--------- .../go-experimental/go-petstore/client.go | 21 +++- .../go-petstore/configuration.go | 25 ++-- 6 files changed, 154 insertions(+), 98 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache index 5a8cc582a989..624e00f058bd 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache @@ -218,16 +218,16 @@ func (c *APIClient) SignRequest( var prefix string // Determine the cryptographic hash to be used for the signature and the body digest. - switch auth.Algorithm { - case HttpSignatureAlgorithmRsaSha512, HttpSignatureAlgorithmHs2019: + switch auth.SigningScheme { + case HttpSigningSchemeRsaSha512, HttpSigningSchemeHs2019: h = crypto.SHA512 prefix = "SHA-512=" - case HttpSignatureAlgorithmRsaSha256: + case HttpSigningSchemeRsaSha256: // This is deprecated and should no longer be used. h = crypto.SHA256 prefix = "SHA-256=" default: - return fmt.Errorf("Unsupported signature algorith: %v", auth.Algorithm) + return fmt.Errorf("Unsupported signature scheme: %v", auth.SigningScheme) } if !h.Available() { return fmt.Errorf("Hash '%v' is not available", h) @@ -295,7 +295,14 @@ func (c *APIClient) SignRequest( var signature []byte switch key := auth.PrivateKey.(type) { case *rsa.PrivateKey: - signature, err = rsa.SignPKCS1v15(rand.Reader, key, h, d) + switch auth.SigningAlgorithm { + case HttpSigningAlgorithmRsaPKCS1v15: + signature, err = rsa.SignPKCS1v15(rand.Reader, key, h, d) + case "", HttpSigningAlgorithmRsaPSS: + signature, err = rsa.SignPSS(rand.Reader, key, h, d, nil) + default: + return fmt.Errorf("Unsupported signing algorithm: '%s'", auth.SigningAlgorithm) + } case *ecdsa.PrivateKey: signature, err = key.Sign(rand.Reader, d, h) //case ed25519.PrivateKey: requires go 1.13 @@ -315,7 +322,7 @@ func (c *APIClient) SignRequest( sb.WriteString(strings.ToLower(header)) } authStr := fmt.Sprintf(`Signature keyId="%s", algorithm="%s", headers="%s", signature="%s"`, - auth.KeyId, auth.Algorithm, sb.String(), base64.StdEncoding.EncodeToString(signature)) + auth.KeyId, auth.SigningScheme, sb.String(), base64.StdEncoding.EncodeToString(signature)) r.Header.Set("Authorization", authStr) return nil } diff --git a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache index 1111081e6155..370a14cb13e0 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache @@ -76,9 +76,12 @@ const ( // be valid. The value MUST be a Unix timestamp integer value. HttpSignatureParameterExpires string = "(expires)" - HttpSignatureAlgorithmHs2019 string = "hs2019" - HttpSignatureAlgorithmRsaSha512 string = "rsa-sha512" // deprecated - HttpSignatureAlgorithmRsaSha256 string = "rsa-sha256" // deprecated + HttpSigningSchemeHs2019 string = "hs2019" + HttpSigningSchemeRsaSha512 string = "rsa-sha512" // deprecated + HttpSigningSchemeRsaSha256 string = "rsa-sha256" // deprecated + + HttpSigningAlgorithmRsaPKCS1v15 string = "PKCS1v15" + HttpSigningAlgorithmRsaPSS string = "PSS" ) // HttpSignatureAuth provides HTTP signature authentication to a request passed @@ -105,10 +108,12 @@ const ( // 2. Set the 'Digest' header in the request body. // 3. Include the 'Digest' header and value in the HTTP signature. type HttpSignatureAuth struct { - KeyId string // A key identifier. - PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. - Algorithm string // The signature algorithm. Supported value is 'hs2019'. - SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. + KeyId string // A key identifier. + PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. + SigningScheme string // The signature scheme, when signing HTTP requests. Supported value is 'hs2019'. + SigningAlgorithm string // The signature algorithm, when signing HTTP requests. + // Supported values are PKCS1v15, PSS. + SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. } // LoadPrivateKey reads the private key from the specified file. diff --git a/samples/client/petstore/go-experimental/auth_test.go b/samples/client/petstore/go-experimental/auth_test.go index dd3d879a96f6..41d2c7e066da 100644 --- a/samples/client/petstore/go-experimental/auth_test.go +++ b/samples/client/petstore/go-experimental/auth_test.go @@ -12,6 +12,7 @@ import ( "net/http" "net/http/httputil" "os" + "regexp" "strings" "testing" "time" @@ -292,7 +293,7 @@ func TestHttpSignaturePrivateKeys(t *testing.T) { { authConfig := sw.HttpSignatureAuth{ KeyId: "my-key-id", - Algorithm: "hs2019", + SigningScheme: "hs2019", SignedHeaders: []string{"Content-Type"}, } // Generate test private key. @@ -306,7 +307,7 @@ func TestHttpSignaturePrivateKeys(t *testing.T) { { authConfig := sw.HttpSignatureAuth{ KeyId: "my-key-id", - Algorithm: "hs2019", + SigningScheme: "hs2019", SignedHeaders: []string{"Content-Type"}, } // Generate test private key. @@ -356,10 +357,11 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth) st // User-Agent: OpenAPI-Generator/1.0.0/go reqb, _ := httputil.DumpRequest(r.Request, true) reqt := (string)(reqb) - fmt.Printf("Request with HTTP signature:\n%v\n", reqt) + fmt.Printf("Request with HTTP signature. Scheme: '%s'. Algorithm: '%s'. Request:\n%v\n", + authConfig.SigningScheme, authConfig.SigningAlgorithm, reqt) var sb bytes.Buffer fmt.Fprintf(&sb, `Authorization: Signature keyId="%s", algorithm="%s", headers="`, - authConfig.KeyId, authConfig.Algorithm) + authConfig.KeyId, authConfig.SigningScheme) for i, header := range authConfig.SignedHeaders { header = strings.ToLower(header) if i > 0 { @@ -373,8 +375,8 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth) st } case "digest": var prefix string - switch authConfig.Algorithm { - case sw.HttpSignatureAlgorithmRsaSha256: + switch authConfig.SigningScheme { + case sw.HttpSigningSchemeRsaSha256: prefix = "SHA-256=" default: prefix = "SHA-512=" @@ -395,8 +397,8 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth) st } func TestHttpSignatureAuth(t *testing.T) { authConfig := sw.HttpSignatureAuth{ - KeyId: "my-key-id", - Algorithm: sw.HttpSignatureAlgorithmHs2019, + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, SignedHeaders: []string{ sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. @@ -411,15 +413,15 @@ func TestHttpSignatureAuth(t *testing.T) { // Test with empty signed headers. The client should automatically add the "(created)" parameter by default. authConfig = sw.HttpSignatureAuth{ KeyId: "my-key-id", - Algorithm: sw.HttpSignatureAlgorithmHs2019, + SigningScheme: sw.HttpSigningSchemeHs2019, SignedHeaders: []string{}, } executeHttpSignatureAuth(t, &authConfig) // Test with deprecated RSA-SHA512, some servers may still support it. authConfig = sw.HttpSignatureAuth{ - KeyId: "my-key-id", - Algorithm: sw.HttpSignatureAlgorithmRsaSha512, + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeRsaSha512, SignedHeaders: []string{ sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. @@ -433,8 +435,8 @@ func TestHttpSignatureAuth(t *testing.T) { // Test with deprecated RSA-SHA256, some servers may still support it. authConfig = sw.HttpSignatureAuth{ - KeyId: "my-key-id", - Algorithm: sw.HttpSignatureAlgorithmRsaSha256, + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeRsaSha256, SignedHeaders: []string{ sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. @@ -449,8 +451,9 @@ func TestHttpSignatureAuth(t *testing.T) { // Test with headers without date. This makes it possible to get a fixed signature, used for unit test purpose. // This should not be used in production code as it could potentially allow replay attacks. authConfig = sw.HttpSignatureAuth{ - KeyId: "my-key-id", - Algorithm: sw.HttpSignatureAlgorithmHs2019, + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SigningAlgorithm: sw.HttpSigningAlgorithmRsaPKCS1v15, SignedHeaders: []string{ sw.HttpSignatureParameterRequestTarget, "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. @@ -467,6 +470,29 @@ func TestHttpSignatureAuth(t *testing.T) { if authorizationHeaderValue != expectedAuthorizationHeader { t.Errorf("Authorization header value is incorrect. Got '%s' but expected '%s'", authorizationHeaderValue, expectedAuthorizationHeader) } + + // Test with PSS signature. The PSS signature creates a new signature every time it is invoked. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SigningAlgorithm: sw.HttpSigningAlgorithmRsaPSS, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + } + authorizationHeaderValue = executeHttpSignatureAuth(t, &authConfig) + expectedSignature = `[a-zA-Z0-9+/]+=` + expectedAuthorizationHeader = fmt.Sprintf( + `Signature keyId="my-key-id", `+ + `algorithm="hs2019", headers="\(request-target\) host content-type digest", `+ + `signature="%s"`, expectedSignature) + re := regexp.MustCompile(expectedAuthorizationHeader) + if !re.MatchString(authorizationHeaderValue) { + t.Errorf("Authorization header value is incorrect. Got '%s' but expected '%s'", authorizationHeaderValue, expectedAuthorizationHeader) + } } func TestDefaultHeader(t *testing.T) { diff --git a/samples/client/petstore/go-experimental/go-petstore/api/openapi.yaml b/samples/client/petstore/go-experimental/go-petstore/api/openapi.yaml index 02fbac649661..b6d60ba0458c 100644 --- a/samples/client/petstore/go-experimental/go-petstore/api/openapi.yaml +++ b/samples/client/petstore/go-experimental/go-petstore/api/openapi.yaml @@ -32,10 +32,10 @@ paths: description: Pet object that needs to be added to the store required: true responses: - "200": + 200: content: {} description: successful operation - "405": + 405: content: {} description: Invalid input security: @@ -59,16 +59,16 @@ paths: description: Pet object that needs to be added to the store required: true responses: - "200": + 200: content: {} description: successful operation - "400": + 400: content: {} description: Invalid ID supplied - "404": + 404: content: {} description: Pet not found - "405": + 405: content: {} description: Validation exception security: @@ -100,7 +100,7 @@ paths: type: array style: form responses: - "200": + 200: content: application/xml: schema: @@ -113,7 +113,7 @@ paths: $ref: '#/components/schemas/Pet' type: array description: successful operation - "400": + 400: content: {} description: Invalid status value security: @@ -141,7 +141,7 @@ paths: type: array style: form responses: - "200": + 200: content: application/xml: schema: @@ -154,7 +154,7 @@ paths: $ref: '#/components/schemas/Pet' type: array description: successful operation - "400": + 400: content: {} description: Invalid tag value security: @@ -180,10 +180,10 @@ paths: format: int64 type: integer responses: - "200": + 200: content: {} description: successful operation - "400": + 400: content: {} description: Invalid pet value security: @@ -205,7 +205,7 @@ paths: format: int64 type: integer responses: - "200": + 200: content: application/xml: schema: @@ -214,10 +214,10 @@ paths: schema: $ref: '#/components/schemas/Pet' description: successful operation - "400": + 400: content: {} description: Invalid ID supplied - "404": + 404: content: {} description: Pet not found security: @@ -247,7 +247,7 @@ paths: description: Updated status of the pet type: string responses: - "405": + 405: content: {} description: Invalid input security: @@ -281,7 +281,7 @@ paths: format: binary type: string responses: - "200": + 200: content: application/json: schema: @@ -299,7 +299,7 @@ paths: description: Returns a map of status codes to quantities operationId: getInventory responses: - "200": + 200: content: application/json: schema: @@ -324,7 +324,7 @@ paths: description: order placed for purchasing the pet required: true responses: - "200": + 200: content: application/xml: schema: @@ -333,7 +333,7 @@ paths: schema: $ref: '#/components/schemas/Order' description: successful operation - "400": + 400: content: {} description: Invalid Order summary: Place an order for a pet @@ -353,10 +353,10 @@ paths: schema: type: string responses: - "400": + 400: content: {} description: Invalid ID supplied - "404": + 404: content: {} description: Order not found summary: Delete purchase order by ID @@ -377,7 +377,7 @@ paths: minimum: 1 type: integer responses: - "200": + 200: content: application/xml: schema: @@ -386,10 +386,10 @@ paths: schema: $ref: '#/components/schemas/Order' description: successful operation - "400": + 400: content: {} description: Invalid ID supplied - "404": + 404: content: {} description: Order not found summary: Find purchase order by ID @@ -471,7 +471,7 @@ paths: schema: type: string responses: - "200": + 200: content: application/xml: schema: @@ -491,7 +491,7 @@ paths: schema: format: date-time type: string - "400": + 400: content: {} description: Invalid username/password supplied summary: Logs user into the system @@ -519,10 +519,10 @@ paths: schema: type: string responses: - "400": + 400: content: {} description: Invalid username supplied - "404": + 404: content: {} description: User not found summary: Delete user @@ -538,7 +538,7 @@ paths: schema: type: string responses: - "200": + 200: content: application/xml: schema: @@ -547,10 +547,10 @@ paths: schema: $ref: '#/components/schemas/User' description: successful operation - "400": + 400: content: {} description: Invalid username supplied - "404": + 404: content: {} description: User not found summary: Get user by user name @@ -574,10 +574,10 @@ paths: description: Updated user object required: true responses: - "400": + 400: content: {} description: Invalid user supplied - "404": + 404: content: {} description: User not found summary: Updated user @@ -596,7 +596,7 @@ paths: description: client model required: true responses: - "200": + 200: content: application/json: schema: @@ -649,7 +649,7 @@ paths: format: int64 type: integer responses: - "400": + 400: content: {} description: Someting wrong summary: Fake endpoint to test group parameters (optional) @@ -747,10 +747,10 @@ paths: - (xyz) type: string responses: - "400": + 400: content: {} description: Invalid request - "404": + 404: content: {} description: Not found summary: To test enum parameters @@ -767,7 +767,7 @@ paths: description: client model required: true responses: - "200": + 200: content: application/json: schema: @@ -861,10 +861,10 @@ paths: - pattern_without_delimiter required: true responses: - "400": + 400: content: {} description: Invalid username supplied - "404": + 404: content: {} description: User not found security: @@ -888,7 +888,7 @@ paths: description: Input number as post body required: false responses: - "200": + 200: content: '*/*': schema: @@ -909,7 +909,7 @@ paths: description: Input string as post body required: false responses: - "200": + 200: content: '*/*': schema: @@ -930,7 +930,7 @@ paths: description: Input boolean as post body required: false responses: - "200": + 200: content: '*/*': schema: @@ -951,7 +951,7 @@ paths: description: Input composite as post body required: false responses: - "200": + 200: content: '*/*': schema: @@ -979,7 +979,7 @@ paths: - param2 required: true responses: - "200": + 200: content: {} description: successful operation summary: test json serialization of form data @@ -998,7 +998,7 @@ paths: description: request body required: true responses: - "200": + 200: content: {} description: successful operation summary: test inline additionalProperties @@ -1021,7 +1021,7 @@ paths: $ref: '#/components/schemas/User' required: true responses: - "200": + 200: content: {} description: Success tags: @@ -1054,7 +1054,7 @@ paths: description: XmlItem Body required: true responses: - "200": + 200: content: {} description: successful operation summary: creates an XmlItem @@ -1073,7 +1073,7 @@ paths: description: client model required: true responses: - "200": + 200: content: application/json: schema: @@ -1095,7 +1095,7 @@ paths: $ref: '#/components/schemas/FileSchemaTestClass' required: true responses: - "200": + 200: content: {} description: Success tags: @@ -1149,7 +1149,7 @@ paths: type: array style: form responses: - "200": + 200: content: {} description: Success tags: @@ -1181,7 +1181,7 @@ paths: - requiredFile required: true responses: - "200": + 200: content: application/json: schema: @@ -1389,7 +1389,7 @@ components: type: integer property: type: string - "123Number": + 123Number: readOnly: true type: integer required: @@ -1397,7 +1397,7 @@ components: type: object xml: name: Name - "200_response": + 200_response: description: Model for testing model name starting with number properties: name: @@ -1666,7 +1666,7 @@ components: type: object List: properties: - "123-list": + 123-list: type: string type: object Client: diff --git a/samples/client/petstore/go-experimental/go-petstore/client.go b/samples/client/petstore/go-experimental/go-petstore/client.go index f6ce8cdcbcac..683b71674979 100644 --- a/samples/client/petstore/go-experimental/go-petstore/client.go +++ b/samples/client/petstore/go-experimental/go-petstore/client.go @@ -229,16 +229,16 @@ func (c *APIClient) SignRequest( var prefix string // Determine the cryptographic hash to be used for the signature and the body digest. - switch auth.Algorithm { - case HttpSignatureAlgorithmRsaSha512, HttpSignatureAlgorithmHs2019: + switch auth.SigningScheme { + case HttpSigningSchemeRsaSha512, HttpSigningSchemeHs2019: h = crypto.SHA512 prefix = "SHA-512=" - case HttpSignatureAlgorithmRsaSha256: + case HttpSigningSchemeRsaSha256: // This is deprecated and should no longer be used. h = crypto.SHA256 prefix = "SHA-256=" default: - return fmt.Errorf("Unsupported signature algorith: %v", auth.Algorithm) + return fmt.Errorf("Unsupported signature scheme: %v", auth.SigningScheme) } if !h.Available() { return fmt.Errorf("Hash '%v' is not available", h) @@ -306,7 +306,16 @@ func (c *APIClient) SignRequest( var signature []byte switch key := auth.PrivateKey.(type) { case *rsa.PrivateKey: - signature, err = rsa.SignPKCS1v15(rand.Reader, key, h, d) + switch auth.SigningAlgorithm { + case HttpSigningAlgorithmRsaPKCS1v15: + fmt.Printf("PKCS1v15 signature\n") + signature, err = rsa.SignPKCS1v15(rand.Reader, key, h, d) + case "", HttpSigningAlgorithmRsaPSS: + fmt.Printf("PSS signature\n") + signature, err = rsa.SignPSS(rand.Reader, key, h, d, nil) + default: + return fmt.Errorf("Unsupported signing algorithm: '%s'", auth.SigningAlgorithm) + } case *ecdsa.PrivateKey: signature, err = key.Sign(rand.Reader, d, h) //case ed25519.PrivateKey: requires go 1.13 @@ -326,7 +335,7 @@ func (c *APIClient) SignRequest( sb.WriteString(strings.ToLower(header)) } authStr := fmt.Sprintf(`Signature keyId="%s", algorithm="%s", headers="%s", signature="%s"`, - auth.KeyId, auth.Algorithm, sb.String(), base64.StdEncoding.EncodeToString(signature)) + auth.KeyId, auth.SigningScheme, sb.String(), base64.StdEncoding.EncodeToString(signature)) r.Header.Set("Authorization", authStr) return nil } diff --git a/samples/client/petstore/go-experimental/go-petstore/configuration.go b/samples/client/petstore/go-experimental/go-petstore/configuration.go index 4fb4abacf773..7a4a97a60573 100644 --- a/samples/client/petstore/go-experimental/go-petstore/configuration.go +++ b/samples/client/petstore/go-experimental/go-petstore/configuration.go @@ -84,13 +84,20 @@ const ( // be valid. The value MUST be a Unix timestamp integer value. HttpSignatureParameterExpires string = "(expires)" - HttpSignatureAlgorithmHs2019 string = "hs2019" - HttpSignatureAlgorithmRsaSha512 string = "rsa-sha512" // deprecated - HttpSignatureAlgorithmRsaSha256 string = "rsa-sha256" // deprecated + HttpSigningSchemeHs2019 string = "hs2019" + HttpSigningSchemeRsaSha512 string = "rsa-sha512" // deprecated + HttpSigningSchemeRsaSha256 string = "rsa-sha256" // deprecated + + HttpSigningAlgorithmRsaPKCS1v15 string = "PKCS1v15" + HttpSigningAlgorithmRsaPSS string = "PSS" ) -// HttpSignatureAuth provides http message signature authentication to a request passed +// HttpSignatureAuth provides HTTP signature authentication to a request passed // via context using ContextHttpSignatureAuth. +// An 'Authorization' header is calculated by creating a hash of select headers, +// and optionally the body of the HTTP request, then signing the hash value using +// a private key which is available to the client. +// // SignedHeaders specifies the list of HTTP headers that are included when generating // the message signature. // The two special signature headers '(request-target)' and '(created)' SHOULD be @@ -109,10 +116,12 @@ const ( // 2. Set the 'Digest' header in the request body. // 3. Include the 'Digest' header and value in the HTTP signature. type HttpSignatureAuth struct { - KeyId string // A key identifier. - PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. - Algorithm string // The signature algorithm. Supported value is 'hs2019'. - SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. + KeyId string // A key identifier. + PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. + SigningScheme string // The signature scheme, when signing HTTP requests. Supported value is 'hs2019'. + SigningAlgorithm string // The signature algorithm, when signing HTTP requests. + // Supported values are PKCS1v15, PSS. + SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. } // LoadPrivateKey reads the private key from the specified file. From 45ec6128ed219bd25337d90b5ac26923c9756e63 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Tue, 14 Jan 2020 10:58:29 -0800 Subject: [PATCH 19/48] Invoke bin scripts --- .../go-petstore/api/openapi.yaml | 112 +++++++++--------- .../go-experimental/go-petstore/client.go | 2 - 2 files changed, 56 insertions(+), 58 deletions(-) diff --git a/samples/client/petstore/go-experimental/go-petstore/api/openapi.yaml b/samples/client/petstore/go-experimental/go-petstore/api/openapi.yaml index b6d60ba0458c..02fbac649661 100644 --- a/samples/client/petstore/go-experimental/go-petstore/api/openapi.yaml +++ b/samples/client/petstore/go-experimental/go-petstore/api/openapi.yaml @@ -32,10 +32,10 @@ paths: description: Pet object that needs to be added to the store required: true responses: - 200: + "200": content: {} description: successful operation - 405: + "405": content: {} description: Invalid input security: @@ -59,16 +59,16 @@ paths: description: Pet object that needs to be added to the store required: true responses: - 200: + "200": content: {} description: successful operation - 400: + "400": content: {} description: Invalid ID supplied - 404: + "404": content: {} description: Pet not found - 405: + "405": content: {} description: Validation exception security: @@ -100,7 +100,7 @@ paths: type: array style: form responses: - 200: + "200": content: application/xml: schema: @@ -113,7 +113,7 @@ paths: $ref: '#/components/schemas/Pet' type: array description: successful operation - 400: + "400": content: {} description: Invalid status value security: @@ -141,7 +141,7 @@ paths: type: array style: form responses: - 200: + "200": content: application/xml: schema: @@ -154,7 +154,7 @@ paths: $ref: '#/components/schemas/Pet' type: array description: successful operation - 400: + "400": content: {} description: Invalid tag value security: @@ -180,10 +180,10 @@ paths: format: int64 type: integer responses: - 200: + "200": content: {} description: successful operation - 400: + "400": content: {} description: Invalid pet value security: @@ -205,7 +205,7 @@ paths: format: int64 type: integer responses: - 200: + "200": content: application/xml: schema: @@ -214,10 +214,10 @@ paths: schema: $ref: '#/components/schemas/Pet' description: successful operation - 400: + "400": content: {} description: Invalid ID supplied - 404: + "404": content: {} description: Pet not found security: @@ -247,7 +247,7 @@ paths: description: Updated status of the pet type: string responses: - 405: + "405": content: {} description: Invalid input security: @@ -281,7 +281,7 @@ paths: format: binary type: string responses: - 200: + "200": content: application/json: schema: @@ -299,7 +299,7 @@ paths: description: Returns a map of status codes to quantities operationId: getInventory responses: - 200: + "200": content: application/json: schema: @@ -324,7 +324,7 @@ paths: description: order placed for purchasing the pet required: true responses: - 200: + "200": content: application/xml: schema: @@ -333,7 +333,7 @@ paths: schema: $ref: '#/components/schemas/Order' description: successful operation - 400: + "400": content: {} description: Invalid Order summary: Place an order for a pet @@ -353,10 +353,10 @@ paths: schema: type: string responses: - 400: + "400": content: {} description: Invalid ID supplied - 404: + "404": content: {} description: Order not found summary: Delete purchase order by ID @@ -377,7 +377,7 @@ paths: minimum: 1 type: integer responses: - 200: + "200": content: application/xml: schema: @@ -386,10 +386,10 @@ paths: schema: $ref: '#/components/schemas/Order' description: successful operation - 400: + "400": content: {} description: Invalid ID supplied - 404: + "404": content: {} description: Order not found summary: Find purchase order by ID @@ -471,7 +471,7 @@ paths: schema: type: string responses: - 200: + "200": content: application/xml: schema: @@ -491,7 +491,7 @@ paths: schema: format: date-time type: string - 400: + "400": content: {} description: Invalid username/password supplied summary: Logs user into the system @@ -519,10 +519,10 @@ paths: schema: type: string responses: - 400: + "400": content: {} description: Invalid username supplied - 404: + "404": content: {} description: User not found summary: Delete user @@ -538,7 +538,7 @@ paths: schema: type: string responses: - 200: + "200": content: application/xml: schema: @@ -547,10 +547,10 @@ paths: schema: $ref: '#/components/schemas/User' description: successful operation - 400: + "400": content: {} description: Invalid username supplied - 404: + "404": content: {} description: User not found summary: Get user by user name @@ -574,10 +574,10 @@ paths: description: Updated user object required: true responses: - 400: + "400": content: {} description: Invalid user supplied - 404: + "404": content: {} description: User not found summary: Updated user @@ -596,7 +596,7 @@ paths: description: client model required: true responses: - 200: + "200": content: application/json: schema: @@ -649,7 +649,7 @@ paths: format: int64 type: integer responses: - 400: + "400": content: {} description: Someting wrong summary: Fake endpoint to test group parameters (optional) @@ -747,10 +747,10 @@ paths: - (xyz) type: string responses: - 400: + "400": content: {} description: Invalid request - 404: + "404": content: {} description: Not found summary: To test enum parameters @@ -767,7 +767,7 @@ paths: description: client model required: true responses: - 200: + "200": content: application/json: schema: @@ -861,10 +861,10 @@ paths: - pattern_without_delimiter required: true responses: - 400: + "400": content: {} description: Invalid username supplied - 404: + "404": content: {} description: User not found security: @@ -888,7 +888,7 @@ paths: description: Input number as post body required: false responses: - 200: + "200": content: '*/*': schema: @@ -909,7 +909,7 @@ paths: description: Input string as post body required: false responses: - 200: + "200": content: '*/*': schema: @@ -930,7 +930,7 @@ paths: description: Input boolean as post body required: false responses: - 200: + "200": content: '*/*': schema: @@ -951,7 +951,7 @@ paths: description: Input composite as post body required: false responses: - 200: + "200": content: '*/*': schema: @@ -979,7 +979,7 @@ paths: - param2 required: true responses: - 200: + "200": content: {} description: successful operation summary: test json serialization of form data @@ -998,7 +998,7 @@ paths: description: request body required: true responses: - 200: + "200": content: {} description: successful operation summary: test inline additionalProperties @@ -1021,7 +1021,7 @@ paths: $ref: '#/components/schemas/User' required: true responses: - 200: + "200": content: {} description: Success tags: @@ -1054,7 +1054,7 @@ paths: description: XmlItem Body required: true responses: - 200: + "200": content: {} description: successful operation summary: creates an XmlItem @@ -1073,7 +1073,7 @@ paths: description: client model required: true responses: - 200: + "200": content: application/json: schema: @@ -1095,7 +1095,7 @@ paths: $ref: '#/components/schemas/FileSchemaTestClass' required: true responses: - 200: + "200": content: {} description: Success tags: @@ -1149,7 +1149,7 @@ paths: type: array style: form responses: - 200: + "200": content: {} description: Success tags: @@ -1181,7 +1181,7 @@ paths: - requiredFile required: true responses: - 200: + "200": content: application/json: schema: @@ -1389,7 +1389,7 @@ components: type: integer property: type: string - 123Number: + "123Number": readOnly: true type: integer required: @@ -1397,7 +1397,7 @@ components: type: object xml: name: Name - 200_response: + "200_response": description: Model for testing model name starting with number properties: name: @@ -1666,7 +1666,7 @@ components: type: object List: properties: - 123-list: + "123-list": type: string type: object Client: diff --git a/samples/client/petstore/go-experimental/go-petstore/client.go b/samples/client/petstore/go-experimental/go-petstore/client.go index 683b71674979..6305477ec1ab 100644 --- a/samples/client/petstore/go-experimental/go-petstore/client.go +++ b/samples/client/petstore/go-experimental/go-petstore/client.go @@ -308,10 +308,8 @@ func (c *APIClient) SignRequest( case *rsa.PrivateKey: switch auth.SigningAlgorithm { case HttpSigningAlgorithmRsaPKCS1v15: - fmt.Printf("PKCS1v15 signature\n") signature, err = rsa.SignPKCS1v15(rand.Reader, key, h, d) case "", HttpSigningAlgorithmRsaPSS: - fmt.Printf("PSS signature\n") signature, err = rsa.SignPSS(rand.Reader, key, h, d, nil) default: return fmt.Errorf("Unsupported signing algorithm: '%s'", auth.SigningAlgorithm) From ca87a61561869d0ae01a85bcdcd4a300f5793275 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Tue, 14 Jan 2020 12:46:12 -0800 Subject: [PATCH 20/48] compliance with HTTP signature draft 12 --- .../resources/go-experimental/client.mustache | 12 +++++- .../petstore/go-experimental/auth_test.go | 39 ++++++++++++------- .../go-experimental/go-petstore/client.go | 12 +++++- 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache index 624e00f058bd..460a18be3955 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache @@ -251,6 +251,7 @@ func (c *APIClient) SignRequest( if len(signedHeaders) == 0 { signedHeaders = []string{HttpSignatureParameterCreated} } + hasCreatedParameter := false for i, header := range signedHeaders { header = strings.ToLower(header) var value string @@ -259,6 +260,7 @@ func (c *APIClient) SignRequest( value = requestTarget case HttpSignatureParameterCreated: value = fmt.Sprintf("%d", created) + hasCreatedParameter = true case "date": value = date r.Header.Set("Date", date) @@ -321,8 +323,14 @@ func (c *APIClient) SignRequest( } sb.WriteString(strings.ToLower(header)) } - authStr := fmt.Sprintf(`Signature keyId="%s", algorithm="%s", headers="%s", signature="%s"`, - auth.KeyId, auth.SigningScheme, sb.String(), base64.StdEncoding.EncodeToString(signature)) + headers_list := sb.String() + sb.Reset() + fmt.Fprintf(&sb, `Signature keyId="%s",algorithm="%s",`, auth.KeyId, auth.SigningScheme) + if hasCreatedParameter { + fmt.Fprintf(&sb, "created=%d,", created) + } + fmt.Fprintf(&sb, `headers="%s",signature="%s"`, headers_list, base64.StdEncoding.EncodeToString(signature)) + authStr := sb.String() r.Header.Set("Authorization", authStr) return nil } diff --git a/samples/client/petstore/go-experimental/auth_test.go b/samples/client/petstore/go-experimental/auth_test.go index 41d2c7e066da..e08b3978844c 100644 --- a/samples/client/petstore/go-experimental/auth_test.go +++ b/samples/client/petstore/go-experimental/auth_test.go @@ -350,7 +350,7 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth) st // GET /v2/pet/12992 HTTP/1.1 // Host: petstore.swagger.io:80 // Accept: application/json - // Authorization: Signature keyId="my-key-id", algorithm="hs2019", headers="(request-target) date host digest content-type", signature="RMJZjVVxzlH02wlxiQgUYDe4QxZaI5IJNIfB2BK8Dhbv3WQ2gw0xyqC+5HiKUmT/cfchhhkUNNsUtiVRnjZmFwtSfYxHfiQvH3KWXlLCMwKGNQC3YzD9lnoWdx0pA5Kxbr0/ygmr3+lTyuN2PJg4IS7Ji/AaKAqIZx7RsHS8Bxw=" + // Authorization: Signature keyId="my-key-id",algorithm="hs2019",created=1579033245,headers="(request-target) date host digest content-type",signature="RMJZjVVxzlH02wlxiQgUYDe4QxZaI5IJNIfB2BK8Dhbv3WQ2gw0xyqC+5HiKUmT/cfchhhkUNNsUtiVRnjZmFwtSfYxHfiQvH3KWXlLCMwKGNQC3YzD9lnoWdx0pA5Kxbr0/ygmr3+lTyuN2PJg4IS7Ji/AaKAqIZx7RsHS8Bxw=" // Date: Tue, 14 Jan 2020 06:41:22 GMT // Digest: SHA-512=z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg== // Testheader: testvalue @@ -360,14 +360,25 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth) st fmt.Printf("Request with HTTP signature. Scheme: '%s'. Algorithm: '%s'. Request:\n%v\n", authConfig.SigningScheme, authConfig.SigningAlgorithm, reqt) var sb bytes.Buffer - fmt.Fprintf(&sb, `Authorization: Signature keyId="%s", algorithm="%s", headers="`, + fmt.Fprintf(&sb, `Signature keyId="%s",algorithm="%s",`, authConfig.KeyId, authConfig.SigningScheme) + if len(authConfig.SignedHeaders) == 0 { + fmt.Fprintf(&sb, `created=[0-9]+,`) + } else { + for _, header := range authConfig.SignedHeaders { + header = strings.ToLower(header) + if header == sw.HttpSignatureParameterCreated { + fmt.Fprintf(&sb, `created=[0-9]+,`) + } + } + } + fmt.Fprintf(&sb, `headers="`) for i, header := range authConfig.SignedHeaders { header = strings.ToLower(header) if i > 0 { fmt.Fprintf(&sb, " ") } - fmt.Fprintf(&sb, header) + fmt.Fprintf(&sb, regexp.QuoteMeta(header)) switch header { case "date": if !strings.Contains(reqt, "Date: ") { @@ -387,11 +398,13 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth) st } } if len(authConfig.SignedHeaders) == 0 { - fmt.Fprintf(&sb, sw.HttpSignatureParameterCreated) + fmt.Fprintf(&sb, regexp.QuoteMeta(sw.HttpSignatureParameterCreated)) } - fmt.Fprintf(&sb, `", signature="`) - if !strings.Contains(reqt, sb.String()) { - t.Errorf("Authorization header is incorrect") + fmt.Fprintf(&sb, `",signature="[a-zA-Z0-9+/]+="`) + re := regexp.MustCompile(sb.String()) + actual := r.Request.Header.Get("Authorization") + if !re.MatchString(actual) { + t.Errorf("Authorization header is incorrect. Expected regex\n'%s'\nbut got\n'%s'", sb.String(), actual) } return r.Request.Header.Get("Authorization") } @@ -464,11 +477,11 @@ func TestHttpSignatureAuth(t *testing.T) { authorizationHeaderValue := executeHttpSignatureAuth(t, &authConfig) expectedSignature := "ED7P+ETthVNx45FZ6Z1lzNDuIE+QuR05GpNJCg7yVGJGDng98inMiPumgdUiljZZr3aEacHXH2Ln8epF4Op6GRGrNYAjXVwK2Nm/6zxt1EEr2JAfA/L5eNjJq9VfZzPjcdanjfqEKvrY6isPpQLsBwRlazhbITsW5E6hAsbVnGk=" expectedAuthorizationHeader := fmt.Sprintf( - `Signature keyId="my-key-id", `+ - `algorithm="hs2019", headers="(request-target) host content-type digest", `+ + `Signature keyId="my-key-id",`+ + `algorithm="hs2019",headers="(request-target) host content-type digest",`+ `signature="%s"`, expectedSignature) if authorizationHeaderValue != expectedAuthorizationHeader { - t.Errorf("Authorization header value is incorrect. Got '%s' but expected '%s'", authorizationHeaderValue, expectedAuthorizationHeader) + t.Errorf("Authorization header value is incorrect. Got\n'%s'\nbut expected\n'%s'", authorizationHeaderValue, expectedAuthorizationHeader) } // Test with PSS signature. The PSS signature creates a new signature every time it is invoked. @@ -486,12 +499,12 @@ func TestHttpSignatureAuth(t *testing.T) { authorizationHeaderValue = executeHttpSignatureAuth(t, &authConfig) expectedSignature = `[a-zA-Z0-9+/]+=` expectedAuthorizationHeader = fmt.Sprintf( - `Signature keyId="my-key-id", `+ - `algorithm="hs2019", headers="\(request-target\) host content-type digest", `+ + `Signature keyId="my-key-id",`+ + `algorithm="hs2019",headers="\(request-target\) host content-type digest",`+ `signature="%s"`, expectedSignature) re := regexp.MustCompile(expectedAuthorizationHeader) if !re.MatchString(authorizationHeaderValue) { - t.Errorf("Authorization header value is incorrect. Got '%s' but expected '%s'", authorizationHeaderValue, expectedAuthorizationHeader) + t.Errorf("Authorization header value is incorrect. Got\n'%s'\nbut expected\n'%s'", authorizationHeaderValue, expectedAuthorizationHeader) } } diff --git a/samples/client/petstore/go-experimental/go-petstore/client.go b/samples/client/petstore/go-experimental/go-petstore/client.go index 6305477ec1ab..21b41b2483b8 100644 --- a/samples/client/petstore/go-experimental/go-petstore/client.go +++ b/samples/client/petstore/go-experimental/go-petstore/client.go @@ -262,6 +262,7 @@ func (c *APIClient) SignRequest( if len(signedHeaders) == 0 { signedHeaders = []string{HttpSignatureParameterCreated} } + hasCreatedParameter := false for i, header := range signedHeaders { header = strings.ToLower(header) var value string @@ -270,6 +271,7 @@ func (c *APIClient) SignRequest( value = requestTarget case HttpSignatureParameterCreated: value = fmt.Sprintf("%d", created) + hasCreatedParameter = true case "date": value = date r.Header.Set("Date", date) @@ -332,8 +334,14 @@ func (c *APIClient) SignRequest( } sb.WriteString(strings.ToLower(header)) } - authStr := fmt.Sprintf(`Signature keyId="%s", algorithm="%s", headers="%s", signature="%s"`, - auth.KeyId, auth.SigningScheme, sb.String(), base64.StdEncoding.EncodeToString(signature)) + headers_list := sb.String() + sb.Reset() + fmt.Fprintf(&sb, `Signature keyId="%s",algorithm="%s",`, auth.KeyId, auth.SigningScheme) + if hasCreatedParameter { + fmt.Fprintf(&sb, "created=%d,", created) + } + fmt.Fprintf(&sb, `headers="%s",signature="%s"`, headers_list, base64.StdEncoding.EncodeToString(signature)) + authStr := sb.String() r.Header.Set("Authorization", authStr) return nil } From 8a6039afc186d506e49f5ecaba768e6edae2dcd6 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Wed, 15 Jan 2020 09:10:59 -0800 Subject: [PATCH 21/48] Support (expires) parameter. Add more unit tests and code comments --- .../resources/go-experimental/client.mustache | 23 ++++- .../go-experimental/configuration.mustache | 25 ++++- .../petstore/go-experimental/auth_test.go | 97 +++++++++++++++---- .../go-experimental/go-petstore/client.go | 23 ++++- .../go-petstore/configuration.go | 25 ++++- 5 files changed, 165 insertions(+), 28 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache index 460a18be3955..266bc15ed971 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache @@ -209,14 +209,22 @@ func (c *APIClient) SignRequest( } now := time.Now() date := now.UTC().Format(http.TimeFormat) - // The `created` field expresses when the signature was created. + // The 'created' field expresses when the signature was created. // The value MUST be a Unix timestamp integer value. See 'HTTP signature' section 2.1.4. created := now.Unix() var h crypto.Hash var err error var prefix string + var expiresUnix float64 + if auth.SignatureMaxValidity < 0 { + return fmt.Errorf("Signature validity must be a positive value") + } + if auth.SignatureMaxValidity > 0 { + e := now.Add(auth.SignatureMaxValidity) + expiresUnix = float64(e.Unix()) + float64(e.Nanosecond()) / float64(time.Second) + } // Determine the cryptographic hash to be used for the signature and the body digest. switch auth.SigningScheme { case HttpSigningSchemeRsaSha512, HttpSigningSchemeHs2019: @@ -252,6 +260,7 @@ func (c *APIClient) SignRequest( signedHeaders = []string{HttpSignatureParameterCreated} } hasCreatedParameter := false + hasExpiresParameter := false for i, header := range signedHeaders { header = strings.ToLower(header) var value string @@ -261,6 +270,12 @@ func (c *APIClient) SignRequest( case HttpSignatureParameterCreated: value = fmt.Sprintf("%d", created) hasCreatedParameter = true + case HttpSignatureParameterExpires: + if auth.SignatureMaxValidity.Nanoseconds() == 0 { + return fmt.Errorf("Cannot set '(expires)' signature parameter. SignatureMaxValidity is not configured.") + } + value = fmt.Sprintf("%.3f", expiresUnix) + hasExpiresParameter = true case "date": value = date r.Header.Set("Date", date) @@ -287,6 +302,9 @@ func (c *APIClient) SignRequest( } fmt.Fprintf(&sb, "%s: %s", header, value) } + if expiresUnix != 0 && !hasExpiresParameter { + return fmt.Errorf("SignatureMaxValidity is specified, but '(expired)' parameter is not present") + } msg := []byte(sb.String()) msgHash := h.New() if _, err = msgHash.Write(msg); err != nil { @@ -329,6 +347,9 @@ func (c *APIClient) SignRequest( if hasCreatedParameter { fmt.Fprintf(&sb, "created=%d,", created) } + if hasExpiresParameter { + fmt.Fprintf(&sb, "expires=%.3f,", expiresUnix) + } fmt.Fprintf(&sb, `headers="%s",signature="%s"`, headers_list, base64.StdEncoding.EncodeToString(signature)) authStr := sb.String() r.Header.Set("Authorization", authStr) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache index 370a14cb13e0..3746c4610b3f 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache @@ -11,6 +11,7 @@ import ( "net/http" "os" "strings" + "time" ) // contextKeys are used to identify the type of value in the context. @@ -76,11 +77,24 @@ const ( // be valid. The value MUST be a Unix timestamp integer value. HttpSignatureParameterExpires string = "(expires)" + // Specifies the Digital Signature Algorithm is derived from metadata + // associated with 'keyId'. Supported DSA algorithms are RSASSA-PKCS1-v1_5, + // RSASSA-PSS, and ECDSA. + // The hash is SHA-512. + // This is the default value. HttpSigningSchemeHs2019 string = "hs2019" - HttpSigningSchemeRsaSha512 string = "rsa-sha512" // deprecated - HttpSigningSchemeRsaSha256 string = "rsa-sha256" // deprecated - + // Use RSASSA-PKCS1-v1_5 with SHA-512 hash. Deprecated. + HttpSigningSchemeRsaSha512 string = "rsa-sha512" + // Use RSASSA-PKCS1-v1_5 with SHA-256 hash. Deprecated. + HttpSigningSchemeRsaSha256 string = "rsa-sha256" + + // RFC 8017 section 7.2 + // Calculate the message signature using RSASSA-PKCS1-V1_5-SIGN from RSA PKCS#1 v1.5. + // PKCSV1_5 is deterministic. The same message and key will produce an identical + // signature value each time. HttpSigningAlgorithmRsaPKCS1v15 string = "PKCS1v15" + // Calculate the message signature using probabilistic signature scheme RSASSA-PSS. + // PSS is randomized and will produce a different signature value each time. HttpSigningAlgorithmRsaPSS string = "PSS" ) @@ -114,6 +128,11 @@ type HttpSignatureAuth struct { SigningAlgorithm string // The signature algorithm, when signing HTTP requests. // Supported values are PKCS1v15, PSS. SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. + // SignatureMaxValidity specifies the maximum duration of the signature validity. + // The value is used to set the '(expires)' signature parameter in the HTTP request. + // '(expires)' is set to '(created)' plus the value of the SignatureMaxValidity field. + // To specify the '(expires)' signature parameter, set 'SignatureMaxValidity' and add '(expires)' to 'SignedHeaders'. + SignatureMaxValidity time.Duration } // LoadPrivateKey reads the private key from the specified file. diff --git a/samples/client/petstore/go-experimental/auth_test.go b/samples/client/petstore/go-experimental/auth_test.go index e08b3978844c..8e4b183ccab9 100644 --- a/samples/client/petstore/go-experimental/auth_test.go +++ b/samples/client/petstore/go-experimental/auth_test.go @@ -319,7 +319,8 @@ func TestHttpSignaturePrivateKeys(t *testing.T) { } */ } -func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth) string { + +func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, expectSuccess bool) string { privateKeyPath := "rsa.pem" writeTestRsaPemKey(t, privateKeyPath) err := authConfig.LoadPrivateKey(privateKeyPath) @@ -341,10 +342,21 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth) st t.Log(r) } + fmt.Printf("Request with HTTP signature. Scheme: '%s'. Algorithm: '%s'. MaxValidity: %v. Headers: '%v'\n", + authConfig.SigningScheme, authConfig.SigningAlgorithm, authConfig.SignatureMaxValidity, authConfig.SignedHeaders) + _, r, err = client.PetApi.GetPetById(auth, 12992) - if err != nil { + if expectSuccess && err != nil { t.Fatalf("Error while deleting pet by id: %v", err) } + if !expectSuccess { + if err == nil { + t.Fatalf("Error was expected, but no error was generated") + } else { + // Do not continue. Error is expected. + return "" + } + } // The request should look like this: // // GET /v2/pet/12992 HTTP/1.1 @@ -357,21 +369,23 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth) st // User-Agent: OpenAPI-Generator/1.0.0/go reqb, _ := httputil.DumpRequest(r.Request, true) reqt := (string)(reqb) - fmt.Printf("Request with HTTP signature. Scheme: '%s'. Algorithm: '%s'. Request:\n%v\n", - authConfig.SigningScheme, authConfig.SigningAlgorithm, reqt) + fmt.Printf("Request:\n%v\n", reqt) var sb bytes.Buffer fmt.Fprintf(&sb, `Signature keyId="%s",algorithm="%s",`, authConfig.KeyId, authConfig.SigningScheme) if len(authConfig.SignedHeaders) == 0 { - fmt.Fprintf(&sb, `created=[0-9]+,`) - } else { - for _, header := range authConfig.SignedHeaders { - header = strings.ToLower(header) - if header == sw.HttpSignatureParameterCreated { - fmt.Fprintf(&sb, `created=[0-9]+,`) - } - } - } + fmt.Fprintf(&sb, `created=[0-9]+,`) + } else { + for _, header := range authConfig.SignedHeaders { + header = strings.ToLower(header) + if header == sw.HttpSignatureParameterCreated { + fmt.Fprintf(&sb, `created=[0-9]+,`) + } + if header == sw.HttpSignatureParameterExpires { + fmt.Fprintf(&sb, `expires=[0-9]+\.[0-9]{3},`) + } + } + } fmt.Fprintf(&sb, `headers="`) for i, header := range authConfig.SignedHeaders { header = strings.ToLower(header) @@ -402,13 +416,15 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth) st } fmt.Fprintf(&sb, `",signature="[a-zA-Z0-9+/]+="`) re := regexp.MustCompile(sb.String()) - actual := r.Request.Header.Get("Authorization") + actual := r.Request.Header.Get("Authorization") if !re.MatchString(actual) { t.Errorf("Authorization header is incorrect. Expected regex\n'%s'\nbut got\n'%s'", sb.String(), actual) } return r.Request.Header.Get("Authorization") } + func TestHttpSignatureAuth(t *testing.T) { + // Test with 'hs2019' signature scheme, and default signature algorithm (RSA SSA PKCS1.5) authConfig := sw.HttpSignatureAuth{ KeyId: "my-key-id", SigningScheme: sw.HttpSigningSchemeHs2019, @@ -421,7 +437,48 @@ func TestHttpSignatureAuth(t *testing.T) { "Digest", // A cryptographic digest of the request body. }, } - executeHttpSignatureAuth(t, &authConfig) + executeHttpSignatureAuth(t, &authConfig, true) + + // Specify signature max validity, but '(expires)' parameter is missing. This should cause an error. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignatureMaxValidity: 7 * time.Minute, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Specify invalid signature max validity. This should cause an error. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignatureMaxValidity: -3 * time.Minute, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Specify signature max validity and '(expires)' parameter. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignatureMaxValidity: time.Minute, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + sw.HttpSignatureParameterExpires, // Time when signature expires. + }, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Specify '(expires)' parameter but no signature max validity. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + sw.HttpSignatureParameterExpires, // Time when signature expires. + }, + } + executeHttpSignatureAuth(t, &authConfig, false) // Test with empty signed headers. The client should automatically add the "(created)" parameter by default. authConfig = sw.HttpSignatureAuth{ @@ -429,7 +486,7 @@ func TestHttpSignatureAuth(t *testing.T) { SigningScheme: sw.HttpSigningSchemeHs2019, SignedHeaders: []string{}, } - executeHttpSignatureAuth(t, &authConfig) + executeHttpSignatureAuth(t, &authConfig, true) // Test with deprecated RSA-SHA512, some servers may still support it. authConfig = sw.HttpSignatureAuth{ @@ -444,7 +501,7 @@ func TestHttpSignatureAuth(t *testing.T) { "Digest", // A cryptographic digest of the request body. }, } - executeHttpSignatureAuth(t, &authConfig) + executeHttpSignatureAuth(t, &authConfig, true) // Test with deprecated RSA-SHA256, some servers may still support it. authConfig = sw.HttpSignatureAuth{ @@ -459,7 +516,7 @@ func TestHttpSignatureAuth(t *testing.T) { "Digest", // A cryptographic digest of the request body. }, } - executeHttpSignatureAuth(t, &authConfig) + executeHttpSignatureAuth(t, &authConfig, true) // Test with headers without date. This makes it possible to get a fixed signature, used for unit test purpose. // This should not be used in production code as it could potentially allow replay attacks. @@ -474,7 +531,7 @@ func TestHttpSignatureAuth(t *testing.T) { "Digest", // A cryptographic digest of the request body. }, } - authorizationHeaderValue := executeHttpSignatureAuth(t, &authConfig) + authorizationHeaderValue := executeHttpSignatureAuth(t, &authConfig, true) expectedSignature := "ED7P+ETthVNx45FZ6Z1lzNDuIE+QuR05GpNJCg7yVGJGDng98inMiPumgdUiljZZr3aEacHXH2Ln8epF4Op6GRGrNYAjXVwK2Nm/6zxt1EEr2JAfA/L5eNjJq9VfZzPjcdanjfqEKvrY6isPpQLsBwRlazhbITsW5E6hAsbVnGk=" expectedAuthorizationHeader := fmt.Sprintf( `Signature keyId="my-key-id",`+ @@ -496,7 +553,7 @@ func TestHttpSignatureAuth(t *testing.T) { "Digest", // A cryptographic digest of the request body. }, } - authorizationHeaderValue = executeHttpSignatureAuth(t, &authConfig) + authorizationHeaderValue = executeHttpSignatureAuth(t, &authConfig, true) expectedSignature = `[a-zA-Z0-9+/]+=` expectedAuthorizationHeader = fmt.Sprintf( `Signature keyId="my-key-id",`+ diff --git a/samples/client/petstore/go-experimental/go-petstore/client.go b/samples/client/petstore/go-experimental/go-petstore/client.go index 21b41b2483b8..e6645f9a5cdf 100644 --- a/samples/client/petstore/go-experimental/go-petstore/client.go +++ b/samples/client/petstore/go-experimental/go-petstore/client.go @@ -220,14 +220,22 @@ func (c *APIClient) SignRequest( } now := time.Now() date := now.UTC().Format(http.TimeFormat) - // The `created` field expresses when the signature was created. + // The 'created' field expresses when the signature was created. // The value MUST be a Unix timestamp integer value. See 'HTTP signature' section 2.1.4. created := now.Unix() var h crypto.Hash var err error var prefix string + var expiresUnix float64 + if auth.SignatureMaxValidity < 0 { + return fmt.Errorf("Signature validity must be a positive value") + } + if auth.SignatureMaxValidity > 0 { + e := now.Add(auth.SignatureMaxValidity) + expiresUnix = float64(e.Unix()) + float64(e.Nanosecond()) / float64(time.Second) + } // Determine the cryptographic hash to be used for the signature and the body digest. switch auth.SigningScheme { case HttpSigningSchemeRsaSha512, HttpSigningSchemeHs2019: @@ -263,6 +271,7 @@ func (c *APIClient) SignRequest( signedHeaders = []string{HttpSignatureParameterCreated} } hasCreatedParameter := false + hasExpiresParameter := false for i, header := range signedHeaders { header = strings.ToLower(header) var value string @@ -272,6 +281,12 @@ func (c *APIClient) SignRequest( case HttpSignatureParameterCreated: value = fmt.Sprintf("%d", created) hasCreatedParameter = true + case HttpSignatureParameterExpires: + if auth.SignatureMaxValidity.Nanoseconds() == 0 { + return fmt.Errorf("Cannot set '(expires)' signature parameter. SignatureMaxValidity is not configured.") + } + value = fmt.Sprintf("%.3f", expiresUnix) + hasExpiresParameter = true case "date": value = date r.Header.Set("Date", date) @@ -298,6 +313,9 @@ func (c *APIClient) SignRequest( } fmt.Fprintf(&sb, "%s: %s", header, value) } + if expiresUnix != 0 && !hasExpiresParameter { + return fmt.Errorf("SignatureMaxValidity is specified, but '(expired)' parameter is not present") + } msg := []byte(sb.String()) msgHash := h.New() if _, err = msgHash.Write(msg); err != nil { @@ -340,6 +358,9 @@ func (c *APIClient) SignRequest( if hasCreatedParameter { fmt.Fprintf(&sb, "created=%d,", created) } + if hasExpiresParameter { + fmt.Fprintf(&sb, "expires=%.3f,", expiresUnix) + } fmt.Fprintf(&sb, `headers="%s",signature="%s"`, headers_list, base64.StdEncoding.EncodeToString(signature)) authStr := sb.String() r.Header.Set("Authorization", authStr) diff --git a/samples/client/petstore/go-experimental/go-petstore/configuration.go b/samples/client/petstore/go-experimental/go-petstore/configuration.go index 7a4a97a60573..7a1871514ac8 100644 --- a/samples/client/petstore/go-experimental/go-petstore/configuration.go +++ b/samples/client/petstore/go-experimental/go-petstore/configuration.go @@ -19,6 +19,7 @@ import ( "net/http" "os" "strings" + "time" ) // contextKeys are used to identify the type of value in the context. @@ -84,11 +85,24 @@ const ( // be valid. The value MUST be a Unix timestamp integer value. HttpSignatureParameterExpires string = "(expires)" + // Specifies the Digital Signature Algorithm is derived from metadata + // associated with 'keyId'. Supported DSA algorithms are RSASSA-PKCS1-v1_5, + // RSASSA-PSS, and ECDSA. + // The hash is SHA-512. + // This is the default value. HttpSigningSchemeHs2019 string = "hs2019" - HttpSigningSchemeRsaSha512 string = "rsa-sha512" // deprecated - HttpSigningSchemeRsaSha256 string = "rsa-sha256" // deprecated - + // Use RSASSA-PKCS1-v1_5 with SHA-512 hash. Deprecated. + HttpSigningSchemeRsaSha512 string = "rsa-sha512" + // Use RSASSA-PKCS1-v1_5 with SHA-256 hash. Deprecated. + HttpSigningSchemeRsaSha256 string = "rsa-sha256" + + // RFC 8017 section 7.2 + // Calculate the message signature using RSASSA-PKCS1-V1_5-SIGN from RSA PKCS#1 v1.5. + // PKCSV1_5 is deterministic. The same message and key will produce an identical + // signature value each time. HttpSigningAlgorithmRsaPKCS1v15 string = "PKCS1v15" + // Calculate the message signature using probabilistic signature scheme RSASSA-PSS. + // PSS is randomized and will produce a different signature value each time. HttpSigningAlgorithmRsaPSS string = "PSS" ) @@ -122,6 +136,11 @@ type HttpSignatureAuth struct { SigningAlgorithm string // The signature algorithm, when signing HTTP requests. // Supported values are PKCS1v15, PSS. SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. + // SignatureMaxValidity specifies the maximum duration of the signature validity. + // The value is used to set the '(expires)' signature parameter in the HTTP request. + // '(expires)' is set to '(created)' plus the value of the SignatureMaxValidity field. + // To specify the '(expires)' signature parameter, set 'SignatureMaxValidity' and add '(expires)' to 'SignedHeaders'. + SignatureMaxValidity time.Duration } // LoadPrivateKey reads the private key from the specified file. From 859ca30ec35c923da304b9b29fb96f515f3ea453 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Wed, 15 Jan 2020 11:02:25 -0800 Subject: [PATCH 22/48] Validate list of signed headers does not have duplicate values --- .../src/main/resources/go-experimental/client.mustache | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache index 266bc15ed971..4553f0bc4f41 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache @@ -259,6 +259,14 @@ func (c *APIClient) SignRequest( if len(signedHeaders) == 0 { signedHeaders = []string{HttpSignatureParameterCreated} } + // Validate the list of signed headers has no duplicates. + m := make(map[string]bool) + for _, h := range signedHeaders { + m[h] = true + } + if len(m) != len(signedHeaders) { + return fmt.Errorf("List of signed headers must not have any duplicates") + } hasCreatedParameter := false hasExpiresParameter := false for i, header := range signedHeaders { From bb124210a23ff595c191e0230c6b35ab86476aa5 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Wed, 15 Jan 2020 15:13:21 -0800 Subject: [PATCH 23/48] move method to ProcessUtils --- .../openapitools/codegen/DefaultGenerator.java | 16 ++-------------- .../codegen/utils/ProcessUtils.java | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java index 161a136b7e06..d1cc734c4cfc 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java @@ -42,6 +42,7 @@ import org.openapitools.codegen.templating.MustacheEngineAdapter; import org.openapitools.codegen.utils.ImplementationVersion; import org.openapitools.codegen.utils.ModelUtils; +import org.openapitools.codegen.utils.ProcessUtils; import org.openapitools.codegen.utils.URLPathUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -851,7 +852,7 @@ private Map buildSupportFileBundle(List allOperations, L if (hasBearerMethods(authMethods)) { bundle.put("hasBearerMethods", true); } - if (hasHttpSignatureMethods(authMethods)) { + if (ProcessUtils.hasHttpSignatureMethods(authMethods)) { bundle.put("hasHttpSignatureMethods", true); } } @@ -1335,19 +1336,6 @@ private boolean hasBearerMethods(List authMethods) { return false; } - // hasHttpSignatureMethods returns true if the specified OAS model has - // HTTP signature methods. - // The HTTP signature scheme is defined in https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ - private boolean hasHttpSignatureMethods(List authMethods) { - for (CodegenSecurity cs : authMethods) { - if (Boolean.TRUE.equals(cs.isHttpSignature)) { - return true; - } - } - - return false; - } - private List getOAuthMethods(List authMethods) { List oauthMethods = new ArrayList<>(); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ProcessUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ProcessUtils.java index c2d7859ecb29..499c1d55308a 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ProcessUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ProcessUtils.java @@ -94,4 +94,22 @@ public static boolean hasBearerMethods(Map objs) { return false; } + /** + * Returns true if the specified OAS model has at least one operation with the HTTP signature + * security scheme. + * The HTTP signature scheme is defined in https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ + * + * @param authMethods List of auth methods. + * @return True if at least one operation has HTTP signature security schema defined + */ + public static boolean hasHttpSignatureMethods(List authMethods) { + if (authMethods != null && !authMethods.isEmpty()) { + for (CodegenSecurity cs : authMethods) { + if (Boolean.TRUE.equals(cs.isHttpSignature)) { + return true; + } + } + } + return false; + } } From 7c1967eff0cfe4f2099120ae0f39ff77d04f2ec1 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Wed, 15 Jan 2020 17:00:30 -0800 Subject: [PATCH 24/48] Code reformatting --- .../java/org/openapitools/codegen/CodegenSecurity.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenSecurity.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenSecurity.java index 62b456803746..809ce85a9643 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenSecurity.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenSecurity.java @@ -31,7 +31,8 @@ public class CodegenSecurity { public Boolean hasMore, isBasic, isOAuth, isApiKey; // is Basic is true for all http authentication type. // Those are to differentiate basic and bearer authentication - // isHttpSignature is to support https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ + // isHttpSignature is to support HTTP signature authorization scheme. + // https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ public Boolean isBasicBasic, isBasicBearer, isHttpSignature; public String bearerFormat; public Map vendorExtensions = new HashMap(); @@ -121,8 +122,9 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(name, type, scheme, hasMore, isBasic, isOAuth, isApiKey, isBasicBasic, isHttpSignature, isBasicBearer, - bearerFormat, vendorExtensions, keyParamName, isKeyInQuery, isKeyInHeader, isKeyInCookie, flow, + return Objects.hash(name, type, scheme, hasMore, isBasic, isOAuth, isApiKey, + isBasicBasic, isHttpSignature, isBasicBearer, bearerFormat, vendorExtensions, + keyParamName, isKeyInQuery, isKeyInHeader, isKeyInCookie, flow, authorizationUrl, tokenUrl, scopes, isCode, isPassword, isApplication, isImplicit); } From 189f276221f9ce418bec164a34ed194cb4dd22bc Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Wed, 15 Jan 2020 19:28:01 -0800 Subject: [PATCH 25/48] Add more validation for HTTP signature and more unit tests. --- .../resources/go-experimental/client.mustache | 37 ++++++++++- .../go-experimental/configuration.mustache | 14 +++-- .../petstore/go-experimental/auth_test.go | 63 ++++++++++++++----- .../go-experimental/go-petstore/client.go | 45 ++++++++++++- .../go-petstore/configuration.go | 14 +++-- 5 files changed, 141 insertions(+), 32 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache index 4553f0bc4f41..9b9d122755a8 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache @@ -18,6 +18,7 @@ import ( "mime/multipart" "net/http" "net/http/httputil" + "net/textproto" "net/url" "os" "path/filepath" @@ -199,6 +200,16 @@ func (c *APIClient) GetConfig() *Configuration { // SignRequest signs the request using HTTP signature. // See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ +// +// Do not add, remove or change headers that are included in the SignedHeaders +// after SignRequest has been invoked; this is because the header values are +// included in the signature. Any subsequent alteration will cause a signature +// verification failure. +// If there are multiple instances of the same header field, all +// header field values associated with the header field MUST be +// concatenated, separated by a ASCII comma and an ASCII space +// ', ', and used in the order in which they will appear in the +// transmitted HTTP message. func (c *APIClient) SignRequest( ctx context.Context, r *http.Request, @@ -273,6 +284,8 @@ func (c *APIClient) SignRequest( header = strings.ToLower(header) var value string switch header { + case HttpSignatureParameterAuthorization: + return fmt.Errorf("Cannot include the 'Authorization' header as a signed header.") case HttpSignatureParameterRequestTarget: value = requestTarget case HttpSignatureParameterCreated: @@ -292,7 +305,14 @@ func (c *APIClient) SignRequest( // Calculate body digest per RFC 3230 section 4.3.2 bodyHash := h.New() if r.Body != nil { - if _, err = io.Copy(bodyHash, r.Body); err != nil { + // Make a copy of the body io.Reader so that we can read the body to calculate the hash, + // then one more time when marshaling the request. + var body io.Reader + body, err = r.GetBody() + if err != nil { + return err + } + if _, err = io.Copy(bodyHash, body); err != nil { return err } } @@ -303,7 +323,20 @@ func (c *APIClient) SignRequest( value = r.Host r.Header.Set("Host", r.Host) default: - value = r.Header.Get(header) + var ok bool + var v []string + canonicalHeader := textproto.CanonicalMIMEHeaderKey(header) + if v, ok = r.Header[canonicalHeader]; !ok { + // If a header specified in the headers parameter cannot be matched with + // a provided header in the message, the implementation MUST produce an error. + return fmt.Errorf("Header '%s' does not exist in the request", canonicalHeader) + } + // If there are multiple instances of the same header field, all + // header field values associated with the header field MUST be + // concatenated, separated by a ASCII comma and an ASCII space + // `, `, and used in the order in which they will appear in the + // transmitted HTTP message. + value = strings.Join(v, ", ") } if i > 0 { fmt.Fprintf(&sb, "\n") diff --git a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache index 3746c4610b3f..e6c2e49a83de 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache @@ -68,14 +68,16 @@ type APIKey struct { const ( // Constants for HTTP signature parameters. // The '(request-target)' parameter concatenates the lowercased :method, an - // ASCII space, and the :path pseudo-headers. + // ASCII space, and the :path pseudo-headers. HttpSignatureParameterRequestTarget string = "(request-target)" // The '(created)' parameter expresses when the signature was - // created. The value MUST be a Unix timestamp integer value. + // created. The value MUST be a Unix timestamp integer value. HttpSignatureParameterCreated string = "(created)" // The '(expires)' parameter expresses when the signature ceases to - // be valid. The value MUST be a Unix timestamp integer value. + // be valid. The value MUST be a Unix timestamp integer value. HttpSignatureParameterExpires string = "(expires)" + // The HTTP Authorization header, as specified in RFC 7235, Section 4.2. + HttpSignatureParameterAuthorization string = "authorization" // Specifies the Digital Signature Algorithm is derived from metadata // associated with 'keyId'. Supported DSA algorithms are RSASSA-PKCS1-v1_5, @@ -92,10 +94,10 @@ const ( // Calculate the message signature using RSASSA-PKCS1-V1_5-SIGN from RSA PKCS#1 v1.5. // PKCSV1_5 is deterministic. The same message and key will produce an identical // signature value each time. - HttpSigningAlgorithmRsaPKCS1v15 string = "PKCS1v15" + HttpSigningAlgorithmRsaPKCS1v15 string = "RSASSA-PKCS1-v1_5" // Calculate the message signature using probabilistic signature scheme RSASSA-PSS. // PSS is randomized and will produce a different signature value each time. - HttpSigningAlgorithmRsaPSS string = "PSS" + HttpSigningAlgorithmRsaPSS string = "RSASSA-PSS" ) // HttpSignatureAuth provides HTTP signature authentication to a request passed @@ -126,7 +128,7 @@ type HttpSignatureAuth struct { PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. SigningScheme string // The signature scheme, when signing HTTP requests. Supported value is 'hs2019'. SigningAlgorithm string // The signature algorithm, when signing HTTP requests. - // Supported values are PKCS1v15, PSS. + // Supported values are RSASSA-PKCS1-v1_5, RSASSA-PSS. SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. // SignatureMaxValidity specifies the maximum duration of the signature validity. // The value is used to set the '(expires)' signature parameter in the HTTP request. diff --git a/samples/client/petstore/go-experimental/auth_test.go b/samples/client/petstore/go-experimental/auth_test.go index 8e4b183ccab9..1a1f9f30d93f 100644 --- a/samples/client/petstore/go-experimental/auth_test.go +++ b/samples/client/petstore/go-experimental/auth_test.go @@ -321,42 +321,49 @@ func TestHttpSignaturePrivateKeys(t *testing.T) { } func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, expectSuccess bool) string { + cfg := sw.NewConfiguration() + cfg.AddDefaultHeader("testheader", "testvalue") + cfg.AddDefaultHeader("Content-Type", "application/json") + cfg.Host = testHost + cfg.Scheme = testScheme + apiClient := sw.NewAPIClient(cfg) + privateKeyPath := "rsa.pem" writeTestRsaPemKey(t, privateKeyPath) err := authConfig.LoadPrivateKey(privateKeyPath) if err != nil { t.Fatalf("Error loading private key: %v", err) } - auth := context.WithValue(context.Background(), sw.ContextHttpSignatureAuth, *authConfig) + authCtx := context.WithValue(context.Background(), sw.ContextHttpSignatureAuth, *authConfig) newPet := (sw.Pet{Id: sw.PtrInt64(12992), Name: "gopher", PhotoUrls: []string{"http://1.com", "http://2.com"}, Status: sw.PtrString("pending"), Tags: &[]sw.Tag{sw.Tag{Id: sw.PtrInt64(1), Name: sw.PtrString("tag2")}}}) - r, err2 := client.PetApi.AddPet(nil, newPet) - - if err2 != nil { - t.Fatalf("Error while adding pet: %v", err2) - } - if r.StatusCode != 200 { - t.Log(r) - } - fmt.Printf("Request with HTTP signature. Scheme: '%s'. Algorithm: '%s'. MaxValidity: %v. Headers: '%v'\n", authConfig.SigningScheme, authConfig.SigningAlgorithm, authConfig.SignatureMaxValidity, authConfig.SignedHeaders) - _, r, err = client.PetApi.GetPetById(auth, 12992) - if expectSuccess && err != nil { - t.Fatalf("Error while deleting pet by id: %v", err) + r, err2 := apiClient.PetApi.AddPet(authCtx, newPet) + if expectSuccess && err2 != nil { + t.Fatalf("Error while adding pet: %v", err2) } if !expectSuccess { - if err == nil { + if err2 == nil { t.Fatalf("Error was expected, but no error was generated") } else { // Do not continue. Error is expected. return "" } } + if r.StatusCode != 200 { + t.Log(r) + } + + _, r, err = apiClient.PetApi.GetPetById(authCtx, 12992) + if expectSuccess && err != nil { + t.Fatalf("Error while deleting pet by id: %v", err) + } + // The request should look like this: // // GET /v2/pet/12992 HTTP/1.1 @@ -369,7 +376,7 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, ex // User-Agent: OpenAPI-Generator/1.0.0/go reqb, _ := httputil.DumpRequest(r.Request, true) reqt := (string)(reqb) - fmt.Printf("Request:\n%v\n", reqt) + fmt.Printf("REQUEST:\n%v\n", reqt) var sb bytes.Buffer fmt.Fprintf(&sb, `Signature keyId="%s",algorithm="%s",`, authConfig.KeyId, authConfig.SigningScheme) @@ -439,6 +446,30 @@ func TestHttpSignatureAuth(t *testing.T) { } executeHttpSignatureAuth(t, &authConfig, true) + // Test with duplicate headers. This is invalid and should be rejected. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{"Host", "Date", "Host"}, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Test with non-existent header. This is invalid and should be rejected. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{"Host", "Date", "Garbage-HeaderDoesNotExist"}, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Test with 'Authorization' header in the signed headers. This is invalid and should be rejected. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{"Host", "Authorization"}, + } + executeHttpSignatureAuth(t, &authConfig, false) + // Specify signature max validity, but '(expires)' parameter is missing. This should cause an error. authConfig = sw.HttpSignatureAuth{ KeyId: "my-key-id", @@ -532,7 +563,7 @@ func TestHttpSignatureAuth(t *testing.T) { }, } authorizationHeaderValue := executeHttpSignatureAuth(t, &authConfig, true) - expectedSignature := "ED7P+ETthVNx45FZ6Z1lzNDuIE+QuR05GpNJCg7yVGJGDng98inMiPumgdUiljZZr3aEacHXH2Ln8epF4Op6GRGrNYAjXVwK2Nm/6zxt1EEr2JAfA/L5eNjJq9VfZzPjcdanjfqEKvrY6isPpQLsBwRlazhbITsW5E6hAsbVnGk=" + expectedSignature := "sXE2MDeW8os6ywv1oUWaFEPFcSPCEb/msQ/NZGKNA9Emm/e42axaAPojzfkZ9Hacyw/iS/5nH4YIkczMgXu3z5fAwFjumxtf3OxbqvUcQ80wvw2/7B5aQmsF6ZwrCFHZ+L/cj9/bg7L1EGUGtdyDzoRKti4zf9QF/03OsP7QljI=" expectedAuthorizationHeader := fmt.Sprintf( `Signature keyId="my-key-id",`+ `algorithm="hs2019",headers="(request-target) host content-type digest",`+ diff --git a/samples/client/petstore/go-experimental/go-petstore/client.go b/samples/client/petstore/go-experimental/go-petstore/client.go index e6645f9a5cdf..593b6c390edd 100644 --- a/samples/client/petstore/go-experimental/go-petstore/client.go +++ b/samples/client/petstore/go-experimental/go-petstore/client.go @@ -26,6 +26,7 @@ import ( "mime/multipart" "net/http" "net/http/httputil" + "net/textproto" "net/url" "os" "path/filepath" @@ -210,6 +211,16 @@ func (c *APIClient) GetConfig() *Configuration { // SignRequest signs the request using HTTP signature. // See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ +// +// Do not add, remove or change headers that are included in the SignedHeaders +// after SignRequest has been invoked; this is because the header values are +// included in the signature. Any subsequent alteration will cause a signature +// verification failure. +// If there are multiple instances of the same header field, all +// header field values associated with the header field MUST be +// concatenated, separated by a ASCII comma and an ASCII space +// ', ', and used in the order in which they will appear in the +// transmitted HTTP message. func (c *APIClient) SignRequest( ctx context.Context, r *http.Request, @@ -270,12 +281,22 @@ func (c *APIClient) SignRequest( if len(signedHeaders) == 0 { signedHeaders = []string{HttpSignatureParameterCreated} } + // Validate the list of signed headers has no duplicates. + m := make(map[string]bool) + for _, h := range signedHeaders { + m[h] = true + } + if len(m) != len(signedHeaders) { + return fmt.Errorf("List of signed headers must not have any duplicates") + } hasCreatedParameter := false hasExpiresParameter := false for i, header := range signedHeaders { header = strings.ToLower(header) var value string switch header { + case HttpSignatureParameterAuthorization: + return fmt.Errorf("Cannot include the 'Authorization' header as a signed header.") case HttpSignatureParameterRequestTarget: value = requestTarget case HttpSignatureParameterCreated: @@ -295,7 +316,14 @@ func (c *APIClient) SignRequest( // Calculate body digest per RFC 3230 section 4.3.2 bodyHash := h.New() if r.Body != nil { - if _, err = io.Copy(bodyHash, r.Body); err != nil { + // Make a copy of the body io.Reader so that we can read the body to calculate the hash, + // then one more time when marshaling the request. + var body io.Reader + body, err = r.GetBody() + if err != nil { + return err + } + if _, err = io.Copy(bodyHash, body); err != nil { return err } } @@ -306,7 +334,20 @@ func (c *APIClient) SignRequest( value = r.Host r.Header.Set("Host", r.Host) default: - value = r.Header.Get(header) + var ok bool + var v []string + canonicalHeader := textproto.CanonicalMIMEHeaderKey(header) + if v, ok = r.Header[canonicalHeader]; !ok { + // If a header specified in the headers parameter cannot be matched with + // a provided header in the message, the implementation MUST produce an error. + return fmt.Errorf("Header '%s' does not exist in the request", canonicalHeader) + } + // If there are multiple instances of the same header field, all + // header field values associated with the header field MUST be + // concatenated, separated by a ASCII comma and an ASCII space + // `, `, and used in the order in which they will appear in the + // transmitted HTTP message. + value = strings.Join(v, ", ") } if i > 0 { fmt.Fprintf(&sb, "\n") diff --git a/samples/client/petstore/go-experimental/go-petstore/configuration.go b/samples/client/petstore/go-experimental/go-petstore/configuration.go index 7a1871514ac8..df83a7258e90 100644 --- a/samples/client/petstore/go-experimental/go-petstore/configuration.go +++ b/samples/client/petstore/go-experimental/go-petstore/configuration.go @@ -76,14 +76,16 @@ type APIKey struct { const ( // Constants for HTTP signature parameters. // The '(request-target)' parameter concatenates the lowercased :method, an - // ASCII space, and the :path pseudo-headers. + // ASCII space, and the :path pseudo-headers. HttpSignatureParameterRequestTarget string = "(request-target)" // The '(created)' parameter expresses when the signature was - // created. The value MUST be a Unix timestamp integer value. + // created. The value MUST be a Unix timestamp integer value. HttpSignatureParameterCreated string = "(created)" // The '(expires)' parameter expresses when the signature ceases to - // be valid. The value MUST be a Unix timestamp integer value. + // be valid. The value MUST be a Unix timestamp integer value. HttpSignatureParameterExpires string = "(expires)" + // The HTTP Authorization header, as specified in RFC 7235, Section 4.2. + HttpSignatureParameterAuthorization string = "authorization" // Specifies the Digital Signature Algorithm is derived from metadata // associated with 'keyId'. Supported DSA algorithms are RSASSA-PKCS1-v1_5, @@ -100,10 +102,10 @@ const ( // Calculate the message signature using RSASSA-PKCS1-V1_5-SIGN from RSA PKCS#1 v1.5. // PKCSV1_5 is deterministic. The same message and key will produce an identical // signature value each time. - HttpSigningAlgorithmRsaPKCS1v15 string = "PKCS1v15" + HttpSigningAlgorithmRsaPKCS1v15 string = "RSASSA-PKCS1-v1_5" // Calculate the message signature using probabilistic signature scheme RSASSA-PSS. // PSS is randomized and will produce a different signature value each time. - HttpSigningAlgorithmRsaPSS string = "PSS" + HttpSigningAlgorithmRsaPSS string = "RSASSA-PSS" ) // HttpSignatureAuth provides HTTP signature authentication to a request passed @@ -134,7 +136,7 @@ type HttpSignatureAuth struct { PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. SigningScheme string // The signature scheme, when signing HTTP requests. Supported value is 'hs2019'. SigningAlgorithm string // The signature algorithm, when signing HTTP requests. - // Supported values are PKCS1v15, PSS. + // Supported values are RSASSA-PKCS1-v1_5, RSASSA-PSS. SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. // SignatureMaxValidity specifies the maximum duration of the signature validity. // The value is used to set the '(expires)' signature parameter in the HTTP request. From c9768d7a7eeb4a8d7accd83e4491d696fde9c850 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Thu, 16 Jan 2020 06:43:23 -0800 Subject: [PATCH 26/48] move http signature to separate file --- .../resources/go-experimental/client.mustache | 207 +---------------- .../go-experimental/signing.mustache | 210 ++++++++++++++++++ 2 files changed, 211 insertions(+), 206 deletions(-) create mode 100644 modules/openapi-generator/src/main/resources/go-experimental/signing.mustache diff --git a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache index 9b9d122755a8..8836fc5f306b 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache @@ -4,11 +4,6 @@ package {{packageName}} import ( "bytes" "context" - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/ecdsa" - "encoding/base64" "encoding/json" "encoding/xml" "errors" @@ -18,7 +13,6 @@ import ( "mime/multipart" "net/http" "net/http/httputil" - "net/textproto" "net/url" "os" "path/filepath" @@ -198,205 +192,6 @@ func (c *APIClient) GetConfig() *Configuration { return c.cfg } -// SignRequest signs the request using HTTP signature. -// See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ -// -// Do not add, remove or change headers that are included in the SignedHeaders -// after SignRequest has been invoked; this is because the header values are -// included in the signature. Any subsequent alteration will cause a signature -// verification failure. -// If there are multiple instances of the same header field, all -// header field values associated with the header field MUST be -// concatenated, separated by a ASCII comma and an ASCII space -// ', ', and used in the order in which they will appear in the -// transmitted HTTP message. -func (c *APIClient) SignRequest( - ctx context.Context, - r *http.Request, - auth HttpSignatureAuth) error { - - if auth.PrivateKey == nil { - return errors.New("Private key is not set") - } - now := time.Now() - date := now.UTC().Format(http.TimeFormat) - // The 'created' field expresses when the signature was created. - // The value MUST be a Unix timestamp integer value. See 'HTTP signature' section 2.1.4. - created := now.Unix() - - var h crypto.Hash - var err error - var prefix string - var expiresUnix float64 - - if auth.SignatureMaxValidity < 0 { - return fmt.Errorf("Signature validity must be a positive value") - } - if auth.SignatureMaxValidity > 0 { - e := now.Add(auth.SignatureMaxValidity) - expiresUnix = float64(e.Unix()) + float64(e.Nanosecond()) / float64(time.Second) - } - // Determine the cryptographic hash to be used for the signature and the body digest. - switch auth.SigningScheme { - case HttpSigningSchemeRsaSha512, HttpSigningSchemeHs2019: - h = crypto.SHA512 - prefix = "SHA-512=" - case HttpSigningSchemeRsaSha256: - // This is deprecated and should no longer be used. - h = crypto.SHA256 - prefix = "SHA-256=" - default: - return fmt.Errorf("Unsupported signature scheme: %v", auth.SigningScheme) - } - if !h.Available() { - return fmt.Errorf("Hash '%v' is not available", h) - } - - // Build the "(request-target)" signature header. - var sb bytes.Buffer - fmt.Fprintf(&sb, "%s %s", strings.ToLower(r.Method), r.URL.EscapedPath()) - if r.URL.RawQuery != "" { - // The ":path" pseudo-header field includes the path and query parts - // of the target URI (the "path-absolute" production and optionally a - // '?' character followed by the "query" production (see Sections 3.3 - // and 3.4 of [RFC3986] - fmt.Fprintf(&sb, "?%s", r.URL.RawQuery) - } - requestTarget := sb.String() - sb.Reset() - - // Build the string to be signed. - signedHeaders := auth.SignedHeaders - if len(signedHeaders) == 0 { - signedHeaders = []string{HttpSignatureParameterCreated} - } - // Validate the list of signed headers has no duplicates. - m := make(map[string]bool) - for _, h := range signedHeaders { - m[h] = true - } - if len(m) != len(signedHeaders) { - return fmt.Errorf("List of signed headers must not have any duplicates") - } - hasCreatedParameter := false - hasExpiresParameter := false - for i, header := range signedHeaders { - header = strings.ToLower(header) - var value string - switch header { - case HttpSignatureParameterAuthorization: - return fmt.Errorf("Cannot include the 'Authorization' header as a signed header.") - case HttpSignatureParameterRequestTarget: - value = requestTarget - case HttpSignatureParameterCreated: - value = fmt.Sprintf("%d", created) - hasCreatedParameter = true - case HttpSignatureParameterExpires: - if auth.SignatureMaxValidity.Nanoseconds() == 0 { - return fmt.Errorf("Cannot set '(expires)' signature parameter. SignatureMaxValidity is not configured.") - } - value = fmt.Sprintf("%.3f", expiresUnix) - hasExpiresParameter = true - case "date": - value = date - r.Header.Set("Date", date) - case "digest": - // Calculate the digest of the HTTP request body. - // Calculate body digest per RFC 3230 section 4.3.2 - bodyHash := h.New() - if r.Body != nil { - // Make a copy of the body io.Reader so that we can read the body to calculate the hash, - // then one more time when marshaling the request. - var body io.Reader - body, err = r.GetBody() - if err != nil { - return err - } - if _, err = io.Copy(bodyHash, body); err != nil { - return err - } - } - d := bodyHash.Sum(nil) - value = prefix + base64.StdEncoding.EncodeToString(d) - r.Header.Set("Digest", value) - case "host": - value = r.Host - r.Header.Set("Host", r.Host) - default: - var ok bool - var v []string - canonicalHeader := textproto.CanonicalMIMEHeaderKey(header) - if v, ok = r.Header[canonicalHeader]; !ok { - // If a header specified in the headers parameter cannot be matched with - // a provided header in the message, the implementation MUST produce an error. - return fmt.Errorf("Header '%s' does not exist in the request", canonicalHeader) - } - // If there are multiple instances of the same header field, all - // header field values associated with the header field MUST be - // concatenated, separated by a ASCII comma and an ASCII space - // `, `, and used in the order in which they will appear in the - // transmitted HTTP message. - value = strings.Join(v, ", ") - } - if i > 0 { - fmt.Fprintf(&sb, "\n") - } - fmt.Fprintf(&sb, "%s: %s", header, value) - } - if expiresUnix != 0 && !hasExpiresParameter { - return fmt.Errorf("SignatureMaxValidity is specified, but '(expired)' parameter is not present") - } - msg := []byte(sb.String()) - msgHash := h.New() - if _, err = msgHash.Write(msg); err != nil { - return err - } - d := msgHash.Sum(nil) - - var signature []byte - switch key := auth.PrivateKey.(type) { - case *rsa.PrivateKey: - switch auth.SigningAlgorithm { - case HttpSigningAlgorithmRsaPKCS1v15: - signature, err = rsa.SignPKCS1v15(rand.Reader, key, h, d) - case "", HttpSigningAlgorithmRsaPSS: - signature, err = rsa.SignPSS(rand.Reader, key, h, d, nil) - default: - return fmt.Errorf("Unsupported signing algorithm: '%s'", auth.SigningAlgorithm) - } - case *ecdsa.PrivateKey: - signature, err = key.Sign(rand.Reader, d, h) - //case ed25519.PrivateKey: requires go 1.13 - // signature, err = key.Sign(rand.Reader, msg, crypto.Hash(0)) - default: - return fmt.Errorf("Unsupported private key") - } - if err != nil { - return err - } - - sb.Reset() - for i, header := range signedHeaders { - if i > 0 { - sb.WriteRune(' ') - } - sb.WriteString(strings.ToLower(header)) - } - headers_list := sb.String() - sb.Reset() - fmt.Fprintf(&sb, `Signature keyId="%s",algorithm="%s",`, auth.KeyId, auth.SigningScheme) - if hasCreatedParameter { - fmt.Fprintf(&sb, "created=%d,", created) - } - if hasExpiresParameter { - fmt.Fprintf(&sb, "expires=%.3f,", expiresUnix) - } - fmt.Fprintf(&sb, `headers="%s",signature="%s"`, headers_list, base64.StdEncoding.EncodeToString(signature)) - authStr := sb.String() - r.Header.Set("Authorization", authStr) - return nil -} - // prepareRequest build the request func (c *APIClient) prepareRequest( ctx context.Context, @@ -561,7 +356,7 @@ func (c *APIClient) prepareRequest( // HTTP Signature Authentication. All request headers must be set (including default headers) // because the headers may be included in the signature. if auth, ok := ctx.Value(ContextHttpSignatureAuth).(HttpSignatureAuth); ok { - err = c.SignRequest(ctx, localVarRequest, auth) + err = SignRequest(ctx, localVarRequest, auth) if err != nil { return nil, err } diff --git a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache new file mode 100644 index 000000000000..bd958a99ab15 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache @@ -0,0 +1,210 @@ +{{>partial_header}} +package {{packageName}} + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/ecdsa" + "encoding/base64" + "net/textproto" +) + +// SignRequest signs the request using HTTP signature. +// See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ +// +// Do not add, remove or change headers that are included in the SignedHeaders +// after SignRequest has been invoked; this is because the header values are +// included in the signature. Any subsequent alteration will cause a signature +// verification failure. +// If there are multiple instances of the same header field, all +// header field values associated with the header field MUST be +// concatenated, separated by a ASCII comma and an ASCII space +// ', ', and used in the order in which they will appear in the +// transmitted HTTP message. +func SignRequest( + ctx context.Context, + r *http.Request, + auth HttpSignatureAuth) error { + + if auth.PrivateKey == nil { + return errors.New("Private key is not set") + } + now := time.Now() + date := now.UTC().Format(http.TimeFormat) + // The 'created' field expresses when the signature was created. + // The value MUST be a Unix timestamp integer value. See 'HTTP signature' section 2.1.4. + created := now.Unix() + + var h crypto.Hash + var err error + var prefix string + var expiresUnix float64 + + if auth.SignatureMaxValidity < 0 { + return fmt.Errorf("Signature validity must be a positive value") + } + if auth.SignatureMaxValidity > 0 { + e := now.Add(auth.SignatureMaxValidity) + expiresUnix = float64(e.Unix()) + float64(e.Nanosecond()) / float64(time.Second) + } + // Determine the cryptographic hash to be used for the signature and the body digest. + switch auth.SigningScheme { + case HttpSigningSchemeRsaSha512, HttpSigningSchemeHs2019: + h = crypto.SHA512 + prefix = "SHA-512=" + case HttpSigningSchemeRsaSha256: + // This is deprecated and should no longer be used. + h = crypto.SHA256 + prefix = "SHA-256=" + default: + return fmt.Errorf("Unsupported signature scheme: %v", auth.SigningScheme) + } + if !h.Available() { + return fmt.Errorf("Hash '%v' is not available", h) + } + + // Build the "(request-target)" signature header. + var sb bytes.Buffer + fmt.Fprintf(&sb, "%s %s", strings.ToLower(r.Method), r.URL.EscapedPath()) + if r.URL.RawQuery != "" { + // The ":path" pseudo-header field includes the path and query parts + // of the target URI (the "path-absolute" production and optionally a + // '?' character followed by the "query" production (see Sections 3.3 + // and 3.4 of [RFC3986] + fmt.Fprintf(&sb, "?%s", r.URL.RawQuery) + } + requestTarget := sb.String() + sb.Reset() + + // Build the string to be signed. + signedHeaders := auth.SignedHeaders + if len(signedHeaders) == 0 { + signedHeaders = []string{HttpSignatureParameterCreated} + } + // Validate the list of signed headers has no duplicates. + m := make(map[string]bool) + for _, h := range signedHeaders { + m[h] = true + } + if len(m) != len(signedHeaders) { + return fmt.Errorf("List of signed headers must not have any duplicates") + } + hasCreatedParameter := false + hasExpiresParameter := false + for i, header := range signedHeaders { + header = strings.ToLower(header) + var value string + switch header { + case HttpSignatureParameterAuthorization: + return fmt.Errorf("Cannot include the 'Authorization' header as a signed header.") + case HttpSignatureParameterRequestTarget: + value = requestTarget + case HttpSignatureParameterCreated: + value = fmt.Sprintf("%d", created) + hasCreatedParameter = true + case HttpSignatureParameterExpires: + if auth.SignatureMaxValidity.Nanoseconds() == 0 { + return fmt.Errorf("Cannot set '(expires)' signature parameter. SignatureMaxValidity is not configured.") + } + value = fmt.Sprintf("%.3f", expiresUnix) + hasExpiresParameter = true + case "date": + value = date + r.Header.Set("Date", date) + case "digest": + // Calculate the digest of the HTTP request body. + // Calculate body digest per RFC 3230 section 4.3.2 + bodyHash := h.New() + if r.Body != nil { + // Make a copy of the body io.Reader so that we can read the body to calculate the hash, + // then one more time when marshaling the request. + var body io.Reader + body, err = r.GetBody() + if err != nil { + return err + } + if _, err = io.Copy(bodyHash, body); err != nil { + return err + } + } + d := bodyHash.Sum(nil) + value = prefix + base64.StdEncoding.EncodeToString(d) + r.Header.Set("Digest", value) + case "host": + value = r.Host + r.Header.Set("Host", r.Host) + default: + var ok bool + var v []string + canonicalHeader := textproto.CanonicalMIMEHeaderKey(header) + if v, ok = r.Header[canonicalHeader]; !ok { + // If a header specified in the headers parameter cannot be matched with + // a provided header in the message, the implementation MUST produce an error. + return fmt.Errorf("Header '%s' does not exist in the request", canonicalHeader) + } + // If there are multiple instances of the same header field, all + // header field values associated with the header field MUST be + // concatenated, separated by a ASCII comma and an ASCII space + // `, `, and used in the order in which they will appear in the + // transmitted HTTP message. + value = strings.Join(v, ", ") + } + if i > 0 { + fmt.Fprintf(&sb, "\n") + } + fmt.Fprintf(&sb, "%s: %s", header, value) + } + if expiresUnix != 0 && !hasExpiresParameter { + return fmt.Errorf("SignatureMaxValidity is specified, but '(expired)' parameter is not present") + } + msg := []byte(sb.String()) + msgHash := h.New() + if _, err = msgHash.Write(msg); err != nil { + return err + } + d := msgHash.Sum(nil) + + var signature []byte + switch key := auth.PrivateKey.(type) { + case *rsa.PrivateKey: + switch auth.SigningAlgorithm { + case HttpSigningAlgorithmRsaPKCS1v15: + signature, err = rsa.SignPKCS1v15(rand.Reader, key, h, d) + case "", HttpSigningAlgorithmRsaPSS: + signature, err = rsa.SignPSS(rand.Reader, key, h, d, nil) + default: + return fmt.Errorf("Unsupported signing algorithm: '%s'", auth.SigningAlgorithm) + } + case *ecdsa.PrivateKey: + signature, err = key.Sign(rand.Reader, d, h) + //case ed25519.PrivateKey: requires go 1.13 + // signature, err = key.Sign(rand.Reader, msg, crypto.Hash(0)) + default: + return fmt.Errorf("Unsupported private key") + } + if err != nil { + return err + } + + sb.Reset() + for i, header := range signedHeaders { + if i > 0 { + sb.WriteRune(' ') + } + sb.WriteString(strings.ToLower(header)) + } + headers_list := sb.String() + sb.Reset() + fmt.Fprintf(&sb, `Signature keyId="%s",algorithm="%s",`, auth.KeyId, auth.SigningScheme) + if hasCreatedParameter { + fmt.Fprintf(&sb, "created=%d,", created) + } + if hasExpiresParameter { + fmt.Fprintf(&sb, "expires=%.3f,", expiresUnix) + } + fmt.Fprintf(&sb, `headers="%s",signature="%s"`, headers_list, base64.StdEncoding.EncodeToString(signature)) + authStr := sb.String() + r.Header.Set("Authorization", authStr) + return nil +} \ No newline at end of file From 4d45c0800a434b425ba88470f69ac010888a4d7c Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Thu, 16 Jan 2020 07:20:48 -0800 Subject: [PATCH 27/48] move http signature to separate file --- .../GoClientExperimentalCodegen.java | 11 + .../resources/go-experimental/client.mustache | 3 +- .../petstore/go-experimental/auth_test.go | 4 +- .../go-experimental/go-petstore/client.go | 216 ------------------ 4 files changed, 15 insertions(+), 219 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoClientExperimentalCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoClientExperimentalCodegen.java index 181363a97289..289d1e45358e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoClientExperimentalCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoClientExperimentalCodegen.java @@ -16,11 +16,14 @@ package org.openapitools.codegen.languages; +import io.swagger.v3.oas.models.security.SecurityScheme; import org.openapitools.codegen.CodegenModel; import org.openapitools.codegen.CodegenProperty; +import org.openapitools.codegen.CodegenSecurity; import org.openapitools.codegen.SupportingFile; import org.openapitools.codegen.meta.GeneratorMetadata; import org.openapitools.codegen.meta.Stability; +import org.openapitools.codegen.utils.ProcessUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,6 +70,14 @@ public String getHelp() { public void processOpts() { super.processOpts(); supportingFiles.add(new SupportingFile("utils.mustache", "", "utils.go")); + + // Generate the 'signing.py' module, but only if the 'HTTP signature' security scheme is specified in the OAS. + Map securitySchemeMap = openAPI != null ? + (openAPI.getComponents() != null ? openAPI.getComponents().getSecuritySchemes() : null) : null; + List authMethods = fromSecurity(securitySchemeMap); + if (ProcessUtils.hasHttpSignatureMethods(authMethods)) { + supportingFiles.add(new SupportingFile("signing.mustache", "", "signing.go")); + } } @Override diff --git a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache index 8836fc5f306b..b4567ea9d9e3 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/client.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache @@ -351,7 +351,7 @@ func (c *APIClient) prepareRequest( for header, value := range c.cfg.DefaultHeader { localVarRequest.Header.Add(header, value) } - +{{#hasHttpSignatureMethods}} if ctx != nil { // HTTP Signature Authentication. All request headers must be set (including default headers) // because the headers may be included in the signature. @@ -362,6 +362,7 @@ func (c *APIClient) prepareRequest( } } } +{{/hasHttpSignatureMethods}} return localVarRequest, nil } diff --git a/samples/client/petstore/go-experimental/auth_test.go b/samples/client/petstore/go-experimental/auth_test.go index 9da04adfdf55..421b44c323ff 100644 --- a/samples/client/petstore/go-experimental/auth_test.go +++ b/samples/client/petstore/go-experimental/auth_test.go @@ -343,7 +343,7 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, ex fmt.Printf("Request with HTTP signature. Scheme: '%s'. Algorithm: '%s'. MaxValidity: %v. Headers: '%v'\n", authConfig.SigningScheme, authConfig.SigningAlgorithm, authConfig.SignatureMaxValidity, authConfig.SignedHeaders) - r, err2 := apiClient.PetApi.AddPet(authCtx, newPet) + r, err2 := apiClient.PetApi.AddPet(authCtx).Body(newPet).Execute() if expectSuccess && err2 != nil { t.Fatalf("Error while adding pet: %v", err2) } @@ -359,7 +359,7 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, ex t.Log(r) } - _, r, err = apiClient.PetApi.GetPetById(authCtx, 12992) + _, r, err = apiClient.PetApi.GetPetById(authCtx, 12992).Execute() if expectSuccess && err != nil { t.Fatalf("Error while deleting pet by id: %v", err) } diff --git a/samples/client/petstore/go-experimental/go-petstore/client.go b/samples/client/petstore/go-experimental/go-petstore/client.go index 593b6c390edd..2f97c129ab15 100644 --- a/samples/client/petstore/go-experimental/go-petstore/client.go +++ b/samples/client/petstore/go-experimental/go-petstore/client.go @@ -12,11 +12,6 @@ package petstore import ( "bytes" "context" - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/ecdsa" - "encoding/base64" "encoding/json" "encoding/xml" "errors" @@ -26,7 +21,6 @@ import ( "mime/multipart" "net/http" "net/http/httputil" - "net/textproto" "net/url" "os" "path/filepath" @@ -209,205 +203,6 @@ func (c *APIClient) GetConfig() *Configuration { return c.cfg } -// SignRequest signs the request using HTTP signature. -// See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ -// -// Do not add, remove or change headers that are included in the SignedHeaders -// after SignRequest has been invoked; this is because the header values are -// included in the signature. Any subsequent alteration will cause a signature -// verification failure. -// If there are multiple instances of the same header field, all -// header field values associated with the header field MUST be -// concatenated, separated by a ASCII comma and an ASCII space -// ', ', and used in the order in which they will appear in the -// transmitted HTTP message. -func (c *APIClient) SignRequest( - ctx context.Context, - r *http.Request, - auth HttpSignatureAuth) error { - - if auth.PrivateKey == nil { - return errors.New("Private key is not set") - } - now := time.Now() - date := now.UTC().Format(http.TimeFormat) - // The 'created' field expresses when the signature was created. - // The value MUST be a Unix timestamp integer value. See 'HTTP signature' section 2.1.4. - created := now.Unix() - - var h crypto.Hash - var err error - var prefix string - var expiresUnix float64 - - if auth.SignatureMaxValidity < 0 { - return fmt.Errorf("Signature validity must be a positive value") - } - if auth.SignatureMaxValidity > 0 { - e := now.Add(auth.SignatureMaxValidity) - expiresUnix = float64(e.Unix()) + float64(e.Nanosecond()) / float64(time.Second) - } - // Determine the cryptographic hash to be used for the signature and the body digest. - switch auth.SigningScheme { - case HttpSigningSchemeRsaSha512, HttpSigningSchemeHs2019: - h = crypto.SHA512 - prefix = "SHA-512=" - case HttpSigningSchemeRsaSha256: - // This is deprecated and should no longer be used. - h = crypto.SHA256 - prefix = "SHA-256=" - default: - return fmt.Errorf("Unsupported signature scheme: %v", auth.SigningScheme) - } - if !h.Available() { - return fmt.Errorf("Hash '%v' is not available", h) - } - - // Build the "(request-target)" signature header. - var sb bytes.Buffer - fmt.Fprintf(&sb, "%s %s", strings.ToLower(r.Method), r.URL.EscapedPath()) - if r.URL.RawQuery != "" { - // The ":path" pseudo-header field includes the path and query parts - // of the target URI (the "path-absolute" production and optionally a - // '?' character followed by the "query" production (see Sections 3.3 - // and 3.4 of [RFC3986] - fmt.Fprintf(&sb, "?%s", r.URL.RawQuery) - } - requestTarget := sb.String() - sb.Reset() - - // Build the string to be signed. - signedHeaders := auth.SignedHeaders - if len(signedHeaders) == 0 { - signedHeaders = []string{HttpSignatureParameterCreated} - } - // Validate the list of signed headers has no duplicates. - m := make(map[string]bool) - for _, h := range signedHeaders { - m[h] = true - } - if len(m) != len(signedHeaders) { - return fmt.Errorf("List of signed headers must not have any duplicates") - } - hasCreatedParameter := false - hasExpiresParameter := false - for i, header := range signedHeaders { - header = strings.ToLower(header) - var value string - switch header { - case HttpSignatureParameterAuthorization: - return fmt.Errorf("Cannot include the 'Authorization' header as a signed header.") - case HttpSignatureParameterRequestTarget: - value = requestTarget - case HttpSignatureParameterCreated: - value = fmt.Sprintf("%d", created) - hasCreatedParameter = true - case HttpSignatureParameterExpires: - if auth.SignatureMaxValidity.Nanoseconds() == 0 { - return fmt.Errorf("Cannot set '(expires)' signature parameter. SignatureMaxValidity is not configured.") - } - value = fmt.Sprintf("%.3f", expiresUnix) - hasExpiresParameter = true - case "date": - value = date - r.Header.Set("Date", date) - case "digest": - // Calculate the digest of the HTTP request body. - // Calculate body digest per RFC 3230 section 4.3.2 - bodyHash := h.New() - if r.Body != nil { - // Make a copy of the body io.Reader so that we can read the body to calculate the hash, - // then one more time when marshaling the request. - var body io.Reader - body, err = r.GetBody() - if err != nil { - return err - } - if _, err = io.Copy(bodyHash, body); err != nil { - return err - } - } - d := bodyHash.Sum(nil) - value = prefix + base64.StdEncoding.EncodeToString(d) - r.Header.Set("Digest", value) - case "host": - value = r.Host - r.Header.Set("Host", r.Host) - default: - var ok bool - var v []string - canonicalHeader := textproto.CanonicalMIMEHeaderKey(header) - if v, ok = r.Header[canonicalHeader]; !ok { - // If a header specified in the headers parameter cannot be matched with - // a provided header in the message, the implementation MUST produce an error. - return fmt.Errorf("Header '%s' does not exist in the request", canonicalHeader) - } - // If there are multiple instances of the same header field, all - // header field values associated with the header field MUST be - // concatenated, separated by a ASCII comma and an ASCII space - // `, `, and used in the order in which they will appear in the - // transmitted HTTP message. - value = strings.Join(v, ", ") - } - if i > 0 { - fmt.Fprintf(&sb, "\n") - } - fmt.Fprintf(&sb, "%s: %s", header, value) - } - if expiresUnix != 0 && !hasExpiresParameter { - return fmt.Errorf("SignatureMaxValidity is specified, but '(expired)' parameter is not present") - } - msg := []byte(sb.String()) - msgHash := h.New() - if _, err = msgHash.Write(msg); err != nil { - return err - } - d := msgHash.Sum(nil) - - var signature []byte - switch key := auth.PrivateKey.(type) { - case *rsa.PrivateKey: - switch auth.SigningAlgorithm { - case HttpSigningAlgorithmRsaPKCS1v15: - signature, err = rsa.SignPKCS1v15(rand.Reader, key, h, d) - case "", HttpSigningAlgorithmRsaPSS: - signature, err = rsa.SignPSS(rand.Reader, key, h, d, nil) - default: - return fmt.Errorf("Unsupported signing algorithm: '%s'", auth.SigningAlgorithm) - } - case *ecdsa.PrivateKey: - signature, err = key.Sign(rand.Reader, d, h) - //case ed25519.PrivateKey: requires go 1.13 - // signature, err = key.Sign(rand.Reader, msg, crypto.Hash(0)) - default: - return fmt.Errorf("Unsupported private key") - } - if err != nil { - return err - } - - sb.Reset() - for i, header := range signedHeaders { - if i > 0 { - sb.WriteRune(' ') - } - sb.WriteString(strings.ToLower(header)) - } - headers_list := sb.String() - sb.Reset() - fmt.Fprintf(&sb, `Signature keyId="%s",algorithm="%s",`, auth.KeyId, auth.SigningScheme) - if hasCreatedParameter { - fmt.Fprintf(&sb, "created=%d,", created) - } - if hasExpiresParameter { - fmt.Fprintf(&sb, "expires=%.3f,", expiresUnix) - } - fmt.Fprintf(&sb, `headers="%s",signature="%s"`, headers_list, base64.StdEncoding.EncodeToString(signature)) - authStr := sb.String() - r.Header.Set("Authorization", authStr) - return nil -} - // prepareRequest build the request func (c *APIClient) prepareRequest( ctx context.Context, @@ -567,17 +362,6 @@ func (c *APIClient) prepareRequest( for header, value := range c.cfg.DefaultHeader { localVarRequest.Header.Add(header, value) } - - if ctx != nil { - // HTTP Signature Authentication. All request headers must be set (including default headers) - // because the headers may be included in the signature. - if auth, ok := ctx.Value(ContextHttpSignatureAuth).(HttpSignatureAuth); ok { - err = c.SignRequest(ctx, localVarRequest, auth) - if err != nil { - return nil, err - } - } - } return localVarRequest, nil } From 1bf286fb797700ab91735adae9c3989b16d819a7 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Thu, 16 Jan 2020 16:17:31 -0800 Subject: [PATCH 28/48] Add http-signature security scheme --- .../petstore-with-fake-endpoints-models-for-testing.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml b/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml index f6f35356afc3..d747440c1dc4 100644 --- a/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml @@ -1168,6 +1168,12 @@ components: type: http scheme: bearer bearerFormat: JWT + http_signature_test: + # Test the 'HTTP signature' security scheme. + # Each HTTP request is cryptographically signed as specified + # in https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ + type: http + scheme: signature schemas: Foo: type: object From 517019545f2258920644e558937b2ab8c28b0794 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Thu, 16 Jan 2020 16:56:48 -0800 Subject: [PATCH 29/48] Run sample scripts for go --- .../go-experimental/go-petstore/README.md | 15 +++ .../go-petstore/api/openapi.yaml | 109 +++++++++--------- 2 files changed, 71 insertions(+), 53 deletions(-) diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md b/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md index d030bad98357..77a389bf6ad5 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md @@ -218,6 +218,21 @@ r, err := client.Service.Operation(auth, args) ``` +### http_signature_test + +- **Type**: HTTP basic authentication + +Example + +```golang +auth := context.WithValue(context.Background(), sw.ContextBasicAuth, sw.BasicAuth{ + UserName: "username", + Password: "password", +}) +r, err := client.Service.Operation(auth, args) +``` + + ### petstore_auth diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/api/openapi.yaml b/samples/openapi3/client/petstore/go-experimental/go-petstore/api/openapi.yaml index c5e930efc566..6fea12f3700a 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/api/openapi.yaml +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/api/openapi.yaml @@ -54,7 +54,7 @@ paths: requestBody: $ref: '#/components/requestBodies/Pet' responses: - 405: + "405": description: Invalid input security: - petstore_auth: @@ -68,11 +68,11 @@ paths: requestBody: $ref: '#/components/requestBodies/Pet' responses: - 400: + "400": description: Invalid ID supplied - 404: + "404": description: Pet not found - 405: + "405": description: Validation exception security: - petstore_auth: @@ -105,7 +105,7 @@ paths: type: array style: form responses: - 200: + "200": content: application/xml: schema: @@ -118,7 +118,7 @@ paths: $ref: '#/components/schemas/Pet' type: array description: successful operation - 400: + "400": description: Invalid status value security: - petstore_auth: @@ -145,7 +145,7 @@ paths: type: array style: form responses: - 200: + "200": content: application/xml: schema: @@ -158,7 +158,7 @@ paths: $ref: '#/components/schemas/Pet' type: array description: successful operation - 400: + "400": description: Invalid tag value security: - petstore_auth: @@ -188,7 +188,7 @@ paths: type: integer style: simple responses: - 400: + "400": description: Invalid pet value security: - petstore_auth: @@ -211,7 +211,7 @@ paths: type: integer style: simple responses: - 200: + "200": content: application/xml: schema: @@ -220,9 +220,9 @@ paths: schema: $ref: '#/components/schemas/Pet' description: successful operation - 400: + "400": description: Invalid ID supplied - 404: + "404": description: Pet not found security: - api_key: [] @@ -255,7 +255,7 @@ paths: type: string type: object responses: - 405: + "405": description: Invalid input security: - petstore_auth: @@ -292,7 +292,7 @@ paths: type: string type: object responses: - 200: + "200": content: application/json: schema: @@ -310,7 +310,7 @@ paths: description: Returns a map of status codes to quantities operationId: getInventory responses: - 200: + "200": content: application/json: schema: @@ -335,7 +335,7 @@ paths: description: order placed for purchasing the pet required: true responses: - 200: + "200": content: application/xml: schema: @@ -344,7 +344,7 @@ paths: schema: $ref: '#/components/schemas/Order' description: successful operation - 400: + "400": description: Invalid Order summary: Place an order for a pet tags: @@ -364,9 +364,9 @@ paths: type: string style: simple responses: - 400: + "400": description: Invalid ID supplied - 404: + "404": description: Order not found summary: Delete purchase order by ID tags: @@ -388,7 +388,7 @@ paths: type: integer style: simple responses: - 200: + "200": content: application/xml: schema: @@ -397,9 +397,9 @@ paths: schema: $ref: '#/components/schemas/Order' description: successful operation - 400: + "400": description: Invalid ID supplied - 404: + "404": description: Order not found summary: Find purchase order by ID tags: @@ -464,7 +464,7 @@ paths: type: string style: form responses: - 200: + "200": content: application/xml: schema: @@ -488,7 +488,7 @@ paths: format: date-time type: string style: simple - 400: + "400": description: Invalid username/password supplied summary: Logs user into the system tags: @@ -516,9 +516,9 @@ paths: type: string style: simple responses: - 400: + "400": description: Invalid username supplied - 404: + "404": description: User not found summary: Delete user tags: @@ -535,7 +535,7 @@ paths: type: string style: simple responses: - 200: + "200": content: application/xml: schema: @@ -544,9 +544,9 @@ paths: schema: $ref: '#/components/schemas/User' description: successful operation - 400: + "400": description: Invalid username supplied - 404: + "404": description: User not found summary: Get user by user name tags: @@ -571,9 +571,9 @@ paths: description: Updated user object required: true responses: - 400: + "400": description: Invalid user supplied - 404: + "404": description: User not found summary: Updated user tags: @@ -585,7 +585,7 @@ paths: requestBody: $ref: '#/components/requestBodies/Client' responses: - 200: + "200": content: application/json: schema: @@ -652,7 +652,7 @@ paths: type: integer style: form responses: - 400: + "400": description: Someting wrong security: - bearer_test: [] @@ -767,9 +767,9 @@ paths: type: string type: object responses: - 400: + "400": description: Invalid request - 404: + "404": description: Not found summary: To test enum parameters tags: @@ -780,7 +780,7 @@ paths: requestBody: $ref: '#/components/requestBodies/Client' responses: - 200: + "200": content: application/json: schema: @@ -873,9 +873,9 @@ paths: - pattern_without_delimiter type: object responses: - 400: + "400": description: Invalid username supplied - 404: + "404": description: User not found security: - http_basic_test: [] @@ -897,7 +897,7 @@ paths: $ref: '#/components/schemas/OuterNumber' description: Input number as post body responses: - 200: + "200": content: '*/*': schema: @@ -916,7 +916,7 @@ paths: $ref: '#/components/schemas/OuterString' description: Input string as post body responses: - 200: + "200": content: '*/*': schema: @@ -935,7 +935,7 @@ paths: $ref: '#/components/schemas/OuterBoolean' description: Input boolean as post body responses: - 200: + "200": content: '*/*': schema: @@ -954,7 +954,7 @@ paths: $ref: '#/components/schemas/OuterComposite' description: Input composite as post body responses: - 200: + "200": content: '*/*': schema: @@ -982,7 +982,7 @@ paths: - param2 type: object responses: - 200: + "200": description: successful operation summary: test json serialization of form data tags: @@ -1000,7 +1000,7 @@ paths: description: request body required: true responses: - 200: + "200": description: successful operation summary: test inline additionalProperties tags: @@ -1023,7 +1023,7 @@ paths: $ref: '#/components/schemas/User' required: true responses: - 200: + "200": description: Success tags: - fake @@ -1034,7 +1034,7 @@ paths: requestBody: $ref: '#/components/requestBodies/Client' responses: - 200: + "200": content: application/json: schema: @@ -1055,7 +1055,7 @@ paths: $ref: '#/components/schemas/FileSchemaTestClass' required: true responses: - 200: + "200": description: Success tags: - fake @@ -1110,7 +1110,7 @@ paths: type: array style: form responses: - 200: + "200": description: Success tags: - fake @@ -1144,7 +1144,7 @@ paths: - requiredFile type: object responses: - 200: + "200": content: application/json: schema: @@ -1160,7 +1160,7 @@ paths: /fake/health: get: responses: - 200: + "200": content: application/json: schema: @@ -1423,14 +1423,14 @@ components: type: integer property: type: string - 123Number: + "123Number": readOnly: true type: integer required: - name xml: name: Name - 200_response: + "200_response": description: Model for testing model name starting with number properties: name: @@ -1608,7 +1608,7 @@ components: type: object List: properties: - 123-list: + "123-list": type: string type: object Client: @@ -2057,3 +2057,6 @@ components: bearerFormat: JWT scheme: bearer type: http + http_signature_test: + scheme: signature + type: http From ef59c0ca45c721b5163126895a01032faa930bda Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Thu, 16 Jan 2020 20:49:27 -0800 Subject: [PATCH 30/48] add http_signature_test to security scheme --- .../3_0/petstore-with-fake-endpoints-models-for-testing.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml b/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml index d747440c1dc4..cf45bf76e379 100644 --- a/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml @@ -43,6 +43,7 @@ paths: '405': description: Invalid input security: + - http_signature_test: [] - petstore_auth: - 'write:pets' - 'read:pets' @@ -62,6 +63,7 @@ paths: '405': description: Validation exception security: + - http_signature_test: [] - petstore_auth: - 'write:pets' - 'read:pets' @@ -107,6 +109,7 @@ paths: '400': description: Invalid status value security: + - http_signature_test: [] - petstore_auth: - 'write:pets' - 'read:pets' @@ -147,6 +150,7 @@ paths: '400': description: Invalid tag value security: + - http_signature_test: [] - petstore_auth: - 'write:pets' - 'read:pets' From f8dea28d02889b1648bfdc353244610e436dde6d Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Fri, 17 Jan 2020 08:21:11 -0800 Subject: [PATCH 31/48] remove http signature from petapi --- .../3_0/petstore-with-fake-endpoints-models-for-testing.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml b/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml index cf45bf76e379..d747440c1dc4 100644 --- a/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml @@ -43,7 +43,6 @@ paths: '405': description: Invalid input security: - - http_signature_test: [] - petstore_auth: - 'write:pets' - 'read:pets' @@ -63,7 +62,6 @@ paths: '405': description: Validation exception security: - - http_signature_test: [] - petstore_auth: - 'write:pets' - 'read:pets' @@ -109,7 +107,6 @@ paths: '400': description: Invalid status value security: - - http_signature_test: [] - petstore_auth: - 'write:pets' - 'read:pets' @@ -150,7 +147,6 @@ paths: '400': description: Invalid tag value security: - - http_signature_test: [] - petstore_auth: - 'write:pets' - 'read:pets' From 21e35f0cfeabef0d5cf51043872ea96bd418406d Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Fri, 17 Jan 2020 08:23:29 -0800 Subject: [PATCH 32/48] Add separate OAS file with support for HTTP signature --- ...odels-for-testing-with-http-signature.yaml | 1776 +++++++++++++++++ 1 file changed, 1776 insertions(+) create mode 100644 modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml diff --git a/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml b/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml new file mode 100644 index 000000000000..cf45bf76e379 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml @@ -0,0 +1,1776 @@ +openapi: 3.0.0 +info: + description: >- + This spec is mainly for testing Petstore server and contains fake endpoints, + models. Please do not use this for any other purpose. Special characters: " + \ + version: 1.0.0 + title: OpenAPI Petstore + license: + name: Apache-2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user +paths: + /foo: + get: + responses: + default: + description: response + content: + application/json: + schema: + type: object + properties: + string: + $ref: '#/components/schemas/Foo' + /pet: + servers: + - url: 'http://petstore.swagger.io/v2' + - url: 'http://path-server-test.petstore.local/v2' + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '405': + description: Invalid input + security: + - http_signature_test: [] + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - http_signature_test: [] + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + style: form + explode: false + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - http_signature_test: [] + - petstore_auth: + - 'write:pets' + - 'read:pets' + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: >- + Multiple tags can be provided with comma separated strings. Use tag1, + tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + style: form + explode: false + schema: + type: array + items: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + security: + - http_signature_test: [] + - petstore_auth: + - 'write:pets' + - 'read:pets' + deprecated: true + '/pet/{petId}': + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - api_key: [] + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: '' + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid pet value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + '/pet/{petId}/uploadImage': + post: + tags: + - pet + summary: uploads an image + description: '' + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + additionalMetadata: + description: Additional data to pass to server + type: string + file: + description: file to upload + type: string + format: binary + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: '' + operationId: placeOrder + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid Order + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + description: order placed for purchasing the pet + required: true + '/store/order/{order_id}': + get: + tags: + - store + summary: Find purchase order by ID + description: >- + For valid response try integer IDs with value <= 5 or > 10. Other values + will generated exceptions + operationId: getOrderById + parameters: + - name: order_id + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + minimum: 1 + maximum: 5 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: >- + For valid response try integer IDs with value < 1000. Anything above + 1000 or nonintegers will generate API errors + operationId: deleteOrder + parameters: + - name: order_id + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + responses: + default: + description: successful operation + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Created user object + required: true + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithArrayInput + responses: + default: + description: successful operation + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithListInput + responses: + default: + description: successful operation + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: '' + operationId: logoutUser + responses: + default: + description: successful operation + '/user/{username}': + get: + tags: + - user + summary: Get user by user name + description: '' + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing. + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/User' + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid user supplied + '404': + description: User not found + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Updated user object + required: true + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid username supplied + '404': + description: User not found + /fake_classname_test: + patch: + tags: + - 'fake_classname_tags 123#$%^' + summary: To test class name in snake case + description: To test class name in snake case + operationId: testClassname + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Client' + security: + - api_key_query: [] + requestBody: + $ref: '#/components/requestBodies/Client' + /fake: + patch: + tags: + - fake + summary: To test "client" model + description: To test "client" model + operationId: testClientModel + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Client' + requestBody: + $ref: '#/components/requestBodies/Client' + get: + tags: + - fake + summary: To test enum parameters + description: To test enum parameters + operationId: testEnumParameters + parameters: + - name: enum_header_string_array + in: header + description: Header parameter enum test (string array) + schema: + type: array + items: + type: string + default: $ + enum: + - '>' + - $ + - name: enum_header_string + in: header + description: Header parameter enum test (string) + schema: + type: string + enum: + - _abc + - '-efg' + - (xyz) + default: '-efg' + - name: enum_query_string_array + in: query + description: Query parameter enum test (string array) + schema: + type: array + items: + type: string + default: $ + enum: + - '>' + - $ + - name: enum_query_string + in: query + description: Query parameter enum test (string) + schema: + type: string + enum: + - _abc + - '-efg' + - (xyz) + default: '-efg' + - name: enum_query_integer + in: query + description: Query parameter enum test (double) + schema: + type: integer + format: int32 + enum: + - 1 + - -2 + - name: enum_query_double + in: query + description: Query parameter enum test (double) + schema: + type: number + format: double + enum: + - 1.1 + - -1.2 + responses: + '400': + description: Invalid request + '404': + description: Not found + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + enum_form_string_array: + description: Form parameter enum test (string array) + type: array + items: + type: string + default: $ + enum: + - '>' + - $ + enum_form_string: + description: Form parameter enum test (string) + type: string + enum: + - _abc + - '-efg' + - (xyz) + default: '-efg' + post: + tags: + - fake + summary: | + Fake endpoint for testing various parameters + 假端點 + 偽のエンドポイント + 가짜 엔드 포인트 + description: | + Fake endpoint for testing various parameters + 假端點 + 偽のエンドポイント + 가짜 엔드 포인트 + operationId: testEndpointParameters + responses: + '400': + description: Invalid username supplied + '404': + description: User not found + security: + - http_basic_test: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + integer: + description: None + type: integer + minimum: 10 + maximum: 100 + int32: + description: None + type: integer + format: int32 + minimum: 20 + maximum: 200 + int64: + description: None + type: integer + format: int64 + number: + description: None + type: number + minimum: 32.1 + maximum: 543.2 + float: + description: None + type: number + format: float + maximum: 987.6 + double: + description: None + type: number + format: double + minimum: 67.8 + maximum: 123.4 + string: + description: None + type: string + pattern: '/[a-z]/i' + pattern_without_delimiter: + description: None + type: string + pattern: '^[A-Z].*' + byte: + description: None + type: string + format: byte + binary: + description: None + type: string + format: binary + date: + description: None + type: string + format: date + dateTime: + description: None + type: string + format: date-time + password: + description: None + type: string + format: password + minLength: 10 + maxLength: 64 + callback: + description: None + type: string + required: + - number + - double + - pattern_without_delimiter + - byte + delete: + tags: + - fake + security: + - bearer_test: [] + summary: Fake endpoint to test group parameters (optional) + description: Fake endpoint to test group parameters (optional) + operationId: testGroupParameters + x-group-parameters: true + parameters: + - name: required_string_group + in: query + description: Required String in group parameters + required: true + schema: + type: integer + - name: required_boolean_group + in: header + description: Required Boolean in group parameters + required: true + schema: + type: boolean + - name: required_int64_group + in: query + description: Required Integer in group parameters + required: true + schema: + type: integer + format: int64 + - name: string_group + in: query + description: String in group parameters + schema: + type: integer + - name: boolean_group + in: header + description: Boolean in group parameters + schema: + type: boolean + - name: int64_group + in: query + description: Integer in group parameters + schema: + type: integer + format: int64 + responses: + '400': + description: Someting wrong + /fake/outer/number: + post: + tags: + - fake + description: Test serialization of outer number types + operationId: fakeOuterNumberSerialize + responses: + '200': + description: Output number + content: + '*/*': + schema: + $ref: '#/components/schemas/OuterNumber' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OuterNumber' + description: Input number as post body + /fake/outer/string: + post: + tags: + - fake + description: Test serialization of outer string types + operationId: fakeOuterStringSerialize + responses: + '200': + description: Output string + content: + '*/*': + schema: + $ref: '#/components/schemas/OuterString' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OuterString' + description: Input string as post body + /fake/outer/boolean: + post: + tags: + - fake + description: Test serialization of outer boolean types + operationId: fakeOuterBooleanSerialize + responses: + '200': + description: Output boolean + content: + '*/*': + schema: + $ref: '#/components/schemas/OuterBoolean' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OuterBoolean' + description: Input boolean as post body + /fake/outer/composite: + post: + tags: + - fake + description: Test serialization of object with outer number type + operationId: fakeOuterCompositeSerialize + responses: + '200': + description: Output composite + content: + '*/*': + schema: + $ref: '#/components/schemas/OuterComposite' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OuterComposite' + description: Input composite as post body + /fake/jsonFormData: + get: + tags: + - fake + summary: test json serialization of form data + description: '' + operationId: testJsonFormData + responses: + '200': + description: successful operation + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + param: + description: field1 + type: string + param2: + description: field2 + type: string + required: + - param + - param2 + /fake/inline-additionalProperties: + post: + tags: + - fake + summary: test inline additionalProperties + description: '' + operationId: testInlineAdditionalProperties + responses: + '200': + description: successful operation + requestBody: + content: + application/json: + schema: + type: object + additionalProperties: + type: string + description: request body + required: true + /fake/body-with-query-params: + put: + tags: + - fake + operationId: testBodyWithQueryParams + parameters: + - name: query + in: query + required: true + schema: + type: string + responses: + '200': + description: Success + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + required: true + /another-fake/dummy: + patch: + tags: + - $another-fake? + summary: To test special tags + description: To test special tags and operation ID starting with number + operationId: '123_test_@#$%_special_tags' + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Client' + requestBody: + $ref: '#/components/requestBodies/Client' + /fake/body-with-file-schema: + put: + tags: + - fake + description: >- + For this test, the body for this request much reference a schema named + `File`. + operationId: testBodyWithFileSchema + responses: + '200': + description: Success + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/FileSchemaTestClass' + required: true + /fake/test-query-paramters: + put: + tags: + - fake + description: To test the collection format in query parameters + operationId: testQueryParameterCollectionFormat + parameters: + - name: pipe + in: query + required: true + schema: + type: array + items: + type: string + - name: ioutil + in: query + required: true + style: form + explode: false + schema: + type: array + items: + type: string + - name: http + in: query + required: true + style: spaceDelimited + schema: + type: array + items: + type: string + - name: url + in: query + required: true + style: form + explode: false + schema: + type: array + items: + type: string + - name: context + in: query + required: true + explode: true + schema: + type: array + items: + type: string + responses: + "200": + description: Success + '/fake/{petId}/uploadImageWithRequiredFile': + post: + tags: + - pet + summary: uploads an image (required) + description: '' + operationId: uploadFileWithRequiredFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + additionalMetadata: + description: Additional data to pass to server + type: string + requiredFile: + description: file to upload + type: string + format: binary + required: + - requiredFile + /fake/health: + get: + tags: + - fake + summary: Health check endpoint + responses: + 200: + description: The instance started successfully + content: + application/json: + schema: + $ref: '#/components/schemas/HealthCheckResult' +servers: + - url: 'http://{server}.swagger.io:{port}/v2' + description: petstore server + variables: + server: + enum: + - 'petstore' + - 'qa-petstore' + - 'dev-petstore' + default: 'petstore' + port: + enum: + - 80 + - 8080 + default: 80 + - url: https://localhost:8080/{version} + description: The local server + variables: + version: + enum: + - 'v1' + - 'v2' + default: 'v2' +components: + requestBodies: + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + description: List of user object + required: true + Client: + content: + application/json: + schema: + $ref: '#/components/schemas/Client' + description: client model + required: true + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + api_key_query: + type: apiKey + name: api_key_query + in: query + http_basic_test: + type: http + scheme: basic + bearer_test: + type: http + scheme: bearer + bearerFormat: JWT + http_signature_test: + # Test the 'HTTP signature' security scheme. + # Each HTTP request is cryptographically signed as specified + # in https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ + type: http + scheme: signature + schemas: + Foo: + type: object + properties: + bar: + $ref: '#/components/schemas/Bar' + Bar: + type: string + default: bar + Order: + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + Category: + type: object + required: + - name + properties: + id: + type: integer + format: int64 + name: + type: string + default: default-name + xml: + name: Category + User: + type: object + properties: + id: + type: integer + format: int64 + x-is-unique: true + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + xml: + name: User + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + x-is-unique: true + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + Return: + description: Model for testing reserved words + properties: + return: + type: integer + format: int32 + xml: + name: Return + Name: + description: Model for testing model name same as property name + required: + - name + properties: + name: + type: integer + format: int32 + snake_case: + readOnly: true + type: integer + format: int32 + property: + type: string + 123Number: + type: integer + readOnly: true + xml: + name: Name + 200_response: + description: Model for testing model name starting with number + properties: + name: + type: integer + format: int32 + class: + type: string + xml: + name: Name + ClassModel: + description: Model for testing model with "_class" property + properties: + _class: + type: string + Dog: + allOf: + - $ref: '#/components/schemas/Animal' + - type: object + properties: + breed: + type: string + Cat: + allOf: + - $ref: '#/components/schemas/Animal' + - type: object + properties: + declawed: + type: boolean + Animal: + type: object + discriminator: + propertyName: className + required: + - className + properties: + className: + type: string + color: + type: string + default: red + AnimalFarm: + type: array + items: + $ref: '#/components/schemas/Animal' + format_test: + type: object + required: + - number + - byte + - date + - password + properties: + integer: + type: integer + maximum: 100 + minimum: 10 + int32: + type: integer + format: int32 + maximum: 200 + minimum: 20 + int64: + type: integer + format: int64 + number: + maximum: 543.2 + minimum: 32.1 + type: number + float: + type: number + format: float + maximum: 987.6 + minimum: 54.3 + double: + type: number + format: double + maximum: 123.4 + minimum: 67.8 + string: + type: string + pattern: '/[a-z]/i' + byte: + type: string + format: byte + binary: + type: string + format: binary + date: + type: string + format: date + dateTime: + type: string + format: date-time + uuid: + type: string + format: uuid + example: 72f98069-206d-4f12-9f12-3d1e525a8e84 + password: + type: string + format: password + maxLength: 64 + minLength: 10 + pattern_with_digits: + description: A string that is a 10 digit number. Can have leading zeros. + type: string + pattern: '^\d{10}$' + pattern_with_digits_and_delimiter: + description: A string starting with 'image_' (case insensitive) and one to three digits following i.e. Image_01. + type: string + pattern: '/^image_\d{1,3}$/i' + EnumClass: + type: string + default: '-efg' + enum: + - _abc + - '-efg' + - (xyz) + Enum_Test: + type: object + required: + - enum_string_required + properties: + enum_string: + type: string + enum: + - UPPER + - lower + - '' + enum_string_required: + type: string + enum: + - UPPER + - lower + - '' + enum_integer: + type: integer + format: int32 + enum: + - 1 + - -1 + enum_number: + type: number + format: double + enum: + - 1.1 + - -1.2 + outerEnum: + $ref: '#/components/schemas/OuterEnum' + outerEnumInteger: + $ref: '#/components/schemas/OuterEnumInteger' + outerEnumDefaultValue: + $ref: '#/components/schemas/OuterEnumDefaultValue' + outerEnumIntegerDefaultValue: + $ref: '#/components/schemas/OuterEnumIntegerDefaultValue' + AdditionalPropertiesClass: + type: object + properties: + map_property: + type: object + additionalProperties: + type: string + map_of_map_property: + type: object + additionalProperties: + type: object + additionalProperties: + type: string + MixedPropertiesAndAdditionalPropertiesClass: + type: object + properties: + uuid: + type: string + format: uuid + dateTime: + type: string + format: date-time + map: + type: object + additionalProperties: + $ref: '#/components/schemas/Animal' + List: + type: object + properties: + 123-list: + type: string + Client: + type: object + properties: + client: + type: string + ReadOnlyFirst: + type: object + properties: + bar: + type: string + readOnly: true + baz: + type: string + hasOnlyReadOnly: + type: object + properties: + bar: + type: string + readOnly: true + foo: + type: string + readOnly: true + Capitalization: + type: object + properties: + smallCamel: + type: string + CapitalCamel: + type: string + small_Snake: + type: string + Capital_Snake: + type: string + SCA_ETH_Flow_Points: + type: string + ATT_NAME: + description: | + Name of the pet + type: string + MapTest: + type: object + properties: + map_map_of_string: + type: object + additionalProperties: + type: object + additionalProperties: + type: string + map_of_enum_string: + type: object + additionalProperties: + type: string + enum: + - UPPER + - lower + direct_map: + type: object + additionalProperties: + type: boolean + indirect_map: + $ref: '#/components/schemas/StringBooleanMap' + ArrayTest: + type: object + properties: + array_of_string: + type: array + items: + type: string + array_array_of_integer: + type: array + items: + type: array + items: + type: integer + format: int64 + array_array_of_model: + type: array + items: + type: array + items: + $ref: '#/components/schemas/ReadOnlyFirst' + NumberOnly: + type: object + properties: + JustNumber: + type: number + ArrayOfNumberOnly: + type: object + properties: + ArrayNumber: + type: array + items: + type: number + ArrayOfArrayOfNumberOnly: + type: object + properties: + ArrayArrayNumber: + type: array + items: + type: array + items: + type: number + EnumArrays: + type: object + properties: + just_symbol: + type: string + enum: + - '>=' + - $ + array_enum: + type: array + items: + type: string + enum: + - fish + - crab + OuterEnum: + nullable: true + type: string + enum: + - placed + - approved + - delivered + OuterEnumInteger: + type: integer + enum: + - 0 + - 1 + - 2 + OuterEnumDefaultValue: + type: string + enum: + - placed + - approved + - delivered + default: placed + OuterEnumIntegerDefaultValue: + type: integer + enum: + - 0 + - 1 + - 2 + default: 0 + OuterComposite: + type: object + properties: + my_number: + $ref: '#/components/schemas/OuterNumber' + my_string: + $ref: '#/components/schemas/OuterString' + my_boolean: + $ref: '#/components/schemas/OuterBoolean' + OuterNumber: + type: number + OuterString: + type: string + OuterBoolean: + type: boolean + x-codegen-body-parameter-name: boolean_post_body + StringBooleanMap: + additionalProperties: + type: boolean + FileSchemaTestClass: + type: object + properties: + file: + $ref: '#/components/schemas/File' + files: + type: array + items: + $ref: '#/components/schemas/File' + File: + type: object + description: Must be named `File` for test. + properties: + sourceURI: + description: Test capitalization + type: string + _special_model.name_: + properties: + '$special[property.name]': + type: integer + format: int64 + xml: + name: '$special[model.name]' + HealthCheckResult: + type: object + properties: + NullableMessage: + nullable: true + type: string + description: Just a string to inform instance is up and running. Make it nullable in hope to get it as pointer in generated model. + NullableClass: + type: object + properties: + integer_prop: + type: integer + nullable: true + number_prop: + type: number + nullable: true + boolean_prop: + type: boolean + nullable: true + string_prop: + type: string + nullable: true + date_prop: + type: string + format: date + nullable: true + datetime_prop: + type: string + format: date-time + nullable: true + array_nullable_prop: + type: array + nullable: true + items: + type: object + array_and_items_nullable_prop: + type: array + nullable: true + items: + type: object + nullable: true + array_items_nullable: + type: array + items: + type: object + nullable: true + object_nullable_prop: + type: object + nullable: true + additionalProperties: + type: object + object_and_items_nullable_prop: + type: object + nullable: true + additionalProperties: + type: object + nullable: true + object_items_nullable: + type: object + additionalProperties: + type: object + nullable: true + additionalProperties: + type: object + nullable: true From 507c925636418c3435e713cabfd534a9ea8f98db Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Fri, 17 Jan 2020 14:21:33 -0800 Subject: [PATCH 33/48] Use modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml for unit test purpose --- bin/openapi3/go-experimental-petstore.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/openapi3/go-experimental-petstore.sh b/bin/openapi3/go-experimental-petstore.sh index db24ff5d6c8c..c6b30c5c544d 100755 --- a/bin/openapi3/go-experimental-petstore.sh +++ b/bin/openapi3/go-experimental-petstore.sh @@ -25,7 +25,8 @@ then mvn -B clean package fi -SPEC="modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml" +#SPEC="modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml" +SPEC="modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml" GENERATOR="go-experimental" STUB_DIR="samples/openapi3/client/petstore/go-experimental/go-petstore" From daf060e87fcd77bc819f5f8226b00b65e50cb48e Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Fri, 17 Jan 2020 14:31:48 -0800 Subject: [PATCH 34/48] remove http signature from petstore-with-fake-endpoints-models-for-testing.yaml --- ...re-with-fake-endpoints-models-for-testing.yaml | 6 ------ .../go-experimental/go-petstore/README.md | 15 --------------- .../go-experimental/go-petstore/api/openapi.yaml | 3 --- 3 files changed, 24 deletions(-) diff --git a/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml b/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml index d747440c1dc4..f6f35356afc3 100644 --- a/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml @@ -1168,12 +1168,6 @@ components: type: http scheme: bearer bearerFormat: JWT - http_signature_test: - # Test the 'HTTP signature' security scheme. - # Each HTTP request is cryptographically signed as specified - # in https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ - type: http - scheme: signature schemas: Foo: type: object diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md b/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md index 77a389bf6ad5..d030bad98357 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md @@ -218,21 +218,6 @@ r, err := client.Service.Operation(auth, args) ``` -### http_signature_test - -- **Type**: HTTP basic authentication - -Example - -```golang -auth := context.WithValue(context.Background(), sw.ContextBasicAuth, sw.BasicAuth{ - UserName: "username", - Password: "password", -}) -r, err := client.Service.Operation(auth, args) -``` - - ### petstore_auth diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/api/openapi.yaml b/samples/openapi3/client/petstore/go-experimental/go-petstore/api/openapi.yaml index 6fea12f3700a..a138e08ef957 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/api/openapi.yaml +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/api/openapi.yaml @@ -2057,6 +2057,3 @@ components: bearerFormat: JWT scheme: bearer type: http - http_signature_test: - scheme: signature - type: http From efab4db94ec6d7adef4da6fdeb43f6bcc3dacbf0 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Fri, 17 Jan 2020 15:49:13 -0800 Subject: [PATCH 35/48] move http signature code to separate file --- .../go-experimental/configuration.mustache | 110 ----------------- .../go-experimental/signing.mustache | 111 +++++++++++++++++- .../go-petstore/configuration.go | 110 ----------------- .../go-experimental/go-petstore/README.md | 15 +++ .../go-petstore/api/openapi.yaml | 7 ++ .../go-experimental/go-petstore/client.go | 99 +--------------- .../go-petstore/configuration.go | 45 ------- .../go-petstore/docs/PetApi.md | 8 +- 8 files changed, 137 insertions(+), 368 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache index e6c2e49a83de..3cda1954db89 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache @@ -3,15 +3,9 @@ package {{packageName}} import ( "context" - "crypto" - "crypto/x509" - "encoding/pem" "fmt" - "io/ioutil" "net/http" - "os" "strings" - "time" ) // contextKeys are used to identify the type of value in the context. @@ -65,110 +59,6 @@ type APIKey struct { Prefix string } -const ( - // Constants for HTTP signature parameters. - // The '(request-target)' parameter concatenates the lowercased :method, an - // ASCII space, and the :path pseudo-headers. - HttpSignatureParameterRequestTarget string = "(request-target)" - // The '(created)' parameter expresses when the signature was - // created. The value MUST be a Unix timestamp integer value. - HttpSignatureParameterCreated string = "(created)" - // The '(expires)' parameter expresses when the signature ceases to - // be valid. The value MUST be a Unix timestamp integer value. - HttpSignatureParameterExpires string = "(expires)" - // The HTTP Authorization header, as specified in RFC 7235, Section 4.2. - HttpSignatureParameterAuthorization string = "authorization" - - // Specifies the Digital Signature Algorithm is derived from metadata - // associated with 'keyId'. Supported DSA algorithms are RSASSA-PKCS1-v1_5, - // RSASSA-PSS, and ECDSA. - // The hash is SHA-512. - // This is the default value. - HttpSigningSchemeHs2019 string = "hs2019" - // Use RSASSA-PKCS1-v1_5 with SHA-512 hash. Deprecated. - HttpSigningSchemeRsaSha512 string = "rsa-sha512" - // Use RSASSA-PKCS1-v1_5 with SHA-256 hash. Deprecated. - HttpSigningSchemeRsaSha256 string = "rsa-sha256" - - // RFC 8017 section 7.2 - // Calculate the message signature using RSASSA-PKCS1-V1_5-SIGN from RSA PKCS#1 v1.5. - // PKCSV1_5 is deterministic. The same message and key will produce an identical - // signature value each time. - HttpSigningAlgorithmRsaPKCS1v15 string = "RSASSA-PKCS1-v1_5" - // Calculate the message signature using probabilistic signature scheme RSASSA-PSS. - // PSS is randomized and will produce a different signature value each time. - HttpSigningAlgorithmRsaPSS string = "RSASSA-PSS" -) - -// HttpSignatureAuth provides HTTP signature authentication to a request passed -// via context using ContextHttpSignatureAuth. -// An 'Authorization' header is calculated by creating a hash of select headers, -// and optionally the body of the HTTP request, then signing the hash value using -// a private key which is available to the client. -// -// SignedHeaders specifies the list of HTTP headers that are included when generating -// the message signature. -// The two special signature headers '(request-target)' and '(created)' SHOULD be -// included in SignedHeaders. -// The '(created)' header expresses when the signature was created. -// The '(request-target)' header is a concatenation of the lowercased :method, an -// ASCII space, and the :path pseudo-headers. -// -// For example, SignedHeaders can be set to: -// (request-target) (created) date host digest -// -// When SignedHeaders is not specified, the client defaults to a single value, '(created)', -// in the list of HTTP headers. -// When SignedHeaders contains the 'Digest' value, the client performs the following operations: -// 1. Calculate a digest of request body, as specified in RFC3230, section 4.3.2. -// 2. Set the 'Digest' header in the request body. -// 3. Include the 'Digest' header and value in the HTTP signature. -type HttpSignatureAuth struct { - KeyId string // A key identifier. - PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. - SigningScheme string // The signature scheme, when signing HTTP requests. Supported value is 'hs2019'. - SigningAlgorithm string // The signature algorithm, when signing HTTP requests. - // Supported values are RSASSA-PKCS1-v1_5, RSASSA-PSS. - SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. - // SignatureMaxValidity specifies the maximum duration of the signature validity. - // The value is used to set the '(expires)' signature parameter in the HTTP request. - // '(expires)' is set to '(created)' plus the value of the SignatureMaxValidity field. - // To specify the '(expires)' signature parameter, set 'SignatureMaxValidity' and add '(expires)' to 'SignedHeaders'. - SignatureMaxValidity time.Duration -} - -// LoadPrivateKey reads the private key from the specified file. -func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { - var file *os.File - file, err = os.Open(filename) - if err != nil { - return err - } - defer func() { - err = file.Close() - }() - var priv []byte - priv, err = ioutil.ReadAll(file) - if err != nil { - return err - } - privPem, _ := pem.Decode(priv) - switch privPem.Type { - case "RSA PRIVATE KEY": - if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privPem.Bytes); err != nil { - return err - } - case "EC PRIVATE KEY": - // https://tools.ietf.org/html/rfc5915 section 4. - if h.PrivateKey, err = x509.ParsePKCS8PrivateKey(privPem.Bytes); err != nil { - return err - } - default: - return fmt.Errorf("Key '%s' is not supported", privPem.Type) - } - return nil -} - // ServerVariable stores the information about a server variable type ServerVariable struct { Description string diff --git a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache index bd958a99ab15..93dd76bd5151 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache @@ -3,13 +3,122 @@ package {{packageName}} import ( "crypto" + "crypto/ecdsa" "crypto/rand" "crypto/rsa" - "crypto/ecdsa" + "crypto/x509" "encoding/base64" + "encoding/pem" + "io/ioutil" "net/textproto" + "os" + "time" +) + +const ( + // Constants for HTTP signature parameters. + // The '(request-target)' parameter concatenates the lowercased :method, an + // ASCII space, and the :path pseudo-headers. + HttpSignatureParameterRequestTarget string = "(request-target)" + // The '(created)' parameter expresses when the signature was + // created. The value MUST be a Unix timestamp integer value. + HttpSignatureParameterCreated string = "(created)" + // The '(expires)' parameter expresses when the signature ceases to + // be valid. The value MUST be a Unix timestamp integer value. + HttpSignatureParameterExpires string = "(expires)" + // The HTTP Authorization header, as specified in RFC 7235, Section 4.2. + HttpSignatureParameterAuthorization string = "authorization" + + // Specifies the Digital Signature Algorithm is derived from metadata + // associated with 'keyId'. Supported DSA algorithms are RSASSA-PKCS1-v1_5, + // RSASSA-PSS, and ECDSA. + // The hash is SHA-512. + // This is the default value. + HttpSigningSchemeHs2019 string = "hs2019" + // Use RSASSA-PKCS1-v1_5 with SHA-512 hash. Deprecated. + HttpSigningSchemeRsaSha512 string = "rsa-sha512" + // Use RSASSA-PKCS1-v1_5 with SHA-256 hash. Deprecated. + HttpSigningSchemeRsaSha256 string = "rsa-sha256" + + // RFC 8017 section 7.2 + // Calculate the message signature using RSASSA-PKCS1-V1_5-SIGN from RSA PKCS#1 v1.5. + // PKCSV1_5 is deterministic. The same message and key will produce an identical + // signature value each time. + HttpSigningAlgorithmRsaPKCS1v15 string = "RSASSA-PKCS1-v1_5" + // Calculate the message signature using probabilistic signature scheme RSASSA-PSS. + // PSS is randomized and will produce a different signature value each time. + HttpSigningAlgorithmRsaPSS string = "RSASSA-PSS" ) +// HttpSignatureAuth provides HTTP signature authentication to a request passed +// via context using ContextHttpSignatureAuth. +// An 'Authorization' header is calculated by creating a hash of select headers, +// and optionally the body of the HTTP request, then signing the hash value using +// a private key which is available to the client. +// +// SignedHeaders specifies the list of HTTP headers that are included when generating +// the message signature. +// The two special signature headers '(request-target)' and '(created)' SHOULD be +// included in SignedHeaders. +// The '(created)' header expresses when the signature was created. +// The '(request-target)' header is a concatenation of the lowercased :method, an +// ASCII space, and the :path pseudo-headers. +// +// For example, SignedHeaders can be set to: +// (request-target) (created) date host digest +// +// When SignedHeaders is not specified, the client defaults to a single value, '(created)', +// in the list of HTTP headers. +// When SignedHeaders contains the 'Digest' value, the client performs the following operations: +// 1. Calculate a digest of request body, as specified in RFC3230, section 4.3.2. +// 2. Set the 'Digest' header in the request body. +// 3. Include the 'Digest' header and value in the HTTP signature. +type HttpSignatureAuth struct { + KeyId string // A key identifier. + PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. + SigningScheme string // The signature scheme, when signing HTTP requests. Supported value is 'hs2019'. + SigningAlgorithm string // The signature algorithm, when signing HTTP requests. + // Supported values are RSASSA-PKCS1-v1_5, RSASSA-PSS. + SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. + // SignatureMaxValidity specifies the maximum duration of the signature validity. + // The value is used to set the '(expires)' signature parameter in the HTTP request. + // '(expires)' is set to '(created)' plus the value of the SignatureMaxValidity field. + // To specify the '(expires)' signature parameter, set 'SignatureMaxValidity' and add '(expires)' to 'SignedHeaders'. + SignatureMaxValidity time.Duration +} + +// LoadPrivateKey reads the private key from the specified file. +func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { + var file *os.File + file, err = os.Open(filename) + if err != nil { + return err + } + defer func() { + err = file.Close() + }() + var priv []byte + priv, err = ioutil.ReadAll(file) + if err != nil { + return err + } + privPem, _ := pem.Decode(priv) + switch privPem.Type { + case "RSA PRIVATE KEY": + if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privPem.Bytes); err != nil { + return err + } + case "EC PRIVATE KEY": + // https://tools.ietf.org/html/rfc5915 section 4. + if h.PrivateKey, err = x509.ParsePKCS8PrivateKey(privPem.Bytes); err != nil { + return err + } + default: + return fmt.Errorf("Key '%s' is not supported", privPem.Type) + } + return nil +} + // SignRequest signs the request using HTTP signature. // See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ // diff --git a/samples/client/petstore/go-experimental/go-petstore/configuration.go b/samples/client/petstore/go-experimental/go-petstore/configuration.go index df83a7258e90..4c503f92637b 100644 --- a/samples/client/petstore/go-experimental/go-petstore/configuration.go +++ b/samples/client/petstore/go-experimental/go-petstore/configuration.go @@ -11,15 +11,9 @@ package petstore import ( "context" - "crypto" - "crypto/x509" - "encoding/pem" "fmt" - "io/ioutil" "net/http" - "os" "strings" - "time" ) // contextKeys are used to identify the type of value in the context. @@ -73,110 +67,6 @@ type APIKey struct { Prefix string } -const ( - // Constants for HTTP signature parameters. - // The '(request-target)' parameter concatenates the lowercased :method, an - // ASCII space, and the :path pseudo-headers. - HttpSignatureParameterRequestTarget string = "(request-target)" - // The '(created)' parameter expresses when the signature was - // created. The value MUST be a Unix timestamp integer value. - HttpSignatureParameterCreated string = "(created)" - // The '(expires)' parameter expresses when the signature ceases to - // be valid. The value MUST be a Unix timestamp integer value. - HttpSignatureParameterExpires string = "(expires)" - // The HTTP Authorization header, as specified in RFC 7235, Section 4.2. - HttpSignatureParameterAuthorization string = "authorization" - - // Specifies the Digital Signature Algorithm is derived from metadata - // associated with 'keyId'. Supported DSA algorithms are RSASSA-PKCS1-v1_5, - // RSASSA-PSS, and ECDSA. - // The hash is SHA-512. - // This is the default value. - HttpSigningSchemeHs2019 string = "hs2019" - // Use RSASSA-PKCS1-v1_5 with SHA-512 hash. Deprecated. - HttpSigningSchemeRsaSha512 string = "rsa-sha512" - // Use RSASSA-PKCS1-v1_5 with SHA-256 hash. Deprecated. - HttpSigningSchemeRsaSha256 string = "rsa-sha256" - - // RFC 8017 section 7.2 - // Calculate the message signature using RSASSA-PKCS1-V1_5-SIGN from RSA PKCS#1 v1.5. - // PKCSV1_5 is deterministic. The same message and key will produce an identical - // signature value each time. - HttpSigningAlgorithmRsaPKCS1v15 string = "RSASSA-PKCS1-v1_5" - // Calculate the message signature using probabilistic signature scheme RSASSA-PSS. - // PSS is randomized and will produce a different signature value each time. - HttpSigningAlgorithmRsaPSS string = "RSASSA-PSS" -) - -// HttpSignatureAuth provides HTTP signature authentication to a request passed -// via context using ContextHttpSignatureAuth. -// An 'Authorization' header is calculated by creating a hash of select headers, -// and optionally the body of the HTTP request, then signing the hash value using -// a private key which is available to the client. -// -// SignedHeaders specifies the list of HTTP headers that are included when generating -// the message signature. -// The two special signature headers '(request-target)' and '(created)' SHOULD be -// included in SignedHeaders. -// The '(created)' header expresses when the signature was created. -// The '(request-target)' header is a concatenation of the lowercased :method, an -// ASCII space, and the :path pseudo-headers. -// -// For example, SignedHeaders can be set to: -// (request-target) (created) date host digest -// -// When SignedHeaders is not specified, the client defaults to a single value, '(created)', -// in the list of HTTP headers. -// When SignedHeaders contains the 'Digest' value, the client performs the following operations: -// 1. Calculate a digest of request body, as specified in RFC3230, section 4.3.2. -// 2. Set the 'Digest' header in the request body. -// 3. Include the 'Digest' header and value in the HTTP signature. -type HttpSignatureAuth struct { - KeyId string // A key identifier. - PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. - SigningScheme string // The signature scheme, when signing HTTP requests. Supported value is 'hs2019'. - SigningAlgorithm string // The signature algorithm, when signing HTTP requests. - // Supported values are RSASSA-PKCS1-v1_5, RSASSA-PSS. - SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. - // SignatureMaxValidity specifies the maximum duration of the signature validity. - // The value is used to set the '(expires)' signature parameter in the HTTP request. - // '(expires)' is set to '(created)' plus the value of the SignatureMaxValidity field. - // To specify the '(expires)' signature parameter, set 'SignatureMaxValidity' and add '(expires)' to 'SignedHeaders'. - SignatureMaxValidity time.Duration -} - -// LoadPrivateKey reads the private key from the specified file. -func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { - var file *os.File - file, err = os.Open(filename) - if err != nil { - return err - } - defer func() { - err = file.Close() - }() - var priv []byte - priv, err = ioutil.ReadAll(file) - if err != nil { - return err - } - privPem, _ := pem.Decode(priv) - switch privPem.Type { - case "RSA PRIVATE KEY": - if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privPem.Bytes); err != nil { - return err - } - case "EC PRIVATE KEY": - // https://tools.ietf.org/html/rfc5915 section 4. - if h.PrivateKey, err = x509.ParsePKCS8PrivateKey(privPem.Bytes); err != nil { - return err - } - default: - return fmt.Errorf("Key '%s' is not supported", privPem.Type) - } - return nil -} - // ServerVariable stores the information about a server variable type ServerVariable struct { Description string diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md b/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md index d030bad98357..77a389bf6ad5 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md @@ -218,6 +218,21 @@ r, err := client.Service.Operation(auth, args) ``` +### http_signature_test + +- **Type**: HTTP basic authentication + +Example + +```golang +auth := context.WithValue(context.Background(), sw.ContextBasicAuth, sw.BasicAuth{ + UserName: "username", + Password: "password", +}) +r, err := client.Service.Operation(auth, args) +``` + + ### petstore_auth diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/api/openapi.yaml b/samples/openapi3/client/petstore/go-experimental/go-petstore/api/openapi.yaml index a138e08ef957..461ffa8592f5 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/api/openapi.yaml +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/api/openapi.yaml @@ -57,6 +57,7 @@ paths: "405": description: Invalid input security: + - http_signature_test: [] - petstore_auth: - write:pets - read:pets @@ -75,6 +76,7 @@ paths: "405": description: Validation exception security: + - http_signature_test: [] - petstore_auth: - write:pets - read:pets @@ -121,6 +123,7 @@ paths: "400": description: Invalid status value security: + - http_signature_test: [] - petstore_auth: - write:pets - read:pets @@ -161,6 +164,7 @@ paths: "400": description: Invalid tag value security: + - http_signature_test: [] - petstore_auth: - write:pets - read:pets @@ -2057,3 +2061,6 @@ components: bearerFormat: JWT scheme: bearer type: http + http_signature_test: + scheme: signature + type: http diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/client.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/client.go index 387b0e00a96a..d10ab1f1826d 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/client.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/client.go @@ -12,11 +12,6 @@ package openapi import ( "bytes" "context" - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/ecdsa" - "encoding/base64" "encoding/json" "encoding/xml" "errors" @@ -211,97 +206,6 @@ func (c *APIClient) GetConfig() *Configuration { return c.cfg } -// signRequest signs the request using HTTP signature. -// See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ -func (c *APIClient) signRequest( - ctx context.Context, - r *http.Request, - auth HttpSignatureAuth) error { - - if auth.PrivateKey == nil { - return errors.New("Private key is not set") - } - date := time.Now().UTC().Format(http.TimeFormat) - var digest string - var h crypto.Hash - var err error - switch auth.Algorithm { - case "rsa-sha512", "hs2019": - h = crypto.SHA512 - digest = "SHA-512=" - case "rsa-sha256": - // This is deprecated and should not longer be used. - h = crypto.SHA256 - digest = "SHA-256=" - default: - return fmt.Errorf("Unsupported signature algorith: %v", auth.Algorithm) - } - if !h.Available() { - return fmt.Errorf("Hash '%v' is not available", h) - } - // Calculate body digest per RFC 3230 section 4.3.2 - bodyHash := h.New() - if r.Body != nil { - if _, err = io.Copy(bodyHash, r.Body); err != nil { - return err - } - } - d := bodyHash.Sum(nil) - digest = digest + base64.StdEncoding.EncodeToString(d) - - // Build the string to be signed. - var sb bytes.Buffer - fmt.Fprintf(&sb, "(request-target): %s %s", strings.ToLower(r.Method), r.URL.EscapedPath()) - if r.URL.RawQuery != "" { - // The ":path" pseudo-header field includes the path and query parts - // of the target URI (the "path-absolute" production and optionally a - // '?' character followed by the "query" production (see Sections 3.3 - // and 3.4 of [RFC3986] - fmt.Fprintf(&sb, "?%s", r.URL.RawQuery) - } - fmt.Fprintf(&sb, "\ndate: %s", date) - fmt.Fprintf(&sb, "\nhost: %s", r.Host) - fmt.Fprintf(&sb, "\ndigest: %s", digest) - for _, header := range auth.SignedHeaders { - fmt.Fprintf(&sb, "\n%s: %s", strings.ToLower(header), r.Header.Get(header)) - } - msg := []byte(sb.String()) - msgHash := h.New() - if _, err = msgHash.Write(msg); err != nil { - return err - } - d = msgHash.Sum(nil) - - var signature []byte - switch key := auth.PrivateKey.(type) { - case *rsa.PrivateKey: - signature, err = rsa.SignPKCS1v15(rand.Reader, key, h, d) - case *ecdsa.PrivateKey: - signature, err = key.Sign(rand.Reader, d, h) - //case ed25519.PrivateKey: requires go 1.13 - // signature, err = key.Sign(rand.Reader, msg, crypto.Hash(0)) - default: - return fmt.Errorf("Unsupported private key") - } - if err != nil { - return err - } - - sb.Reset() - sb.WriteString("(request-target) date host digest") - for _, h := range auth.SignedHeaders { - sb.WriteRune(' ') - sb.WriteString(strings.ToLower(h)) - } - r.Header.Set("Host", r.Host) - r.Header.Set("Date", date) - r.Header.Set("Digest", digest) - authStr := fmt.Sprintf(`Signature keyId="%s", algorithm="%s", headers="%s", signature="%s"`, - auth.KeyId, auth.Algorithm, sb.String(), base64.StdEncoding.EncodeToString(signature)) - r.Header.Set("Authorization", authStr) - return nil -} - // prepareRequest build the request func (c *APIClient) prepareRequest( ctx context.Context, @@ -461,12 +365,11 @@ func (c *APIClient) prepareRequest( for header, value := range c.cfg.DefaultHeader { localVarRequest.Header.Add(header, value) } - if ctx != nil { // HTTP Signature Authentication. All request headers must be set (including default headers) // because the headers may be included in the signature. if auth, ok := ctx.Value(ContextHttpSignatureAuth).(HttpSignatureAuth); ok { - err = c.signRequest(ctx, localVarRequest, auth) + err = SignRequest(ctx, localVarRequest, auth) if err != nil { return nil, err } diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/configuration.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/configuration.go index e2cbac5216cd..efba09afc5c5 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/configuration.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/configuration.go @@ -11,13 +11,8 @@ package openapi import ( "context" - "crypto" - "crypto/x509" - "encoding/pem" "fmt" - "io/ioutil" "net/http" - "os" "strings" ) @@ -72,46 +67,6 @@ type APIKey struct { Prefix string } -// HttpSignatureAuth provides http message signature authentication to a request passed via context using ContextHttpSignatureAuth -type HttpSignatureAuth struct { - KeyId string // A key identifier. - PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. - Algorithm string // The signature algorithm. Supported values are rsa-sha256, rsa-sha512, hs2019. - SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. -} - -// LoadPrivateKey reads the private key from the specified file. -func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { - var file *os.File - file, err = os.Open(filename) - if err != nil { - return err - } - defer func() { - err = file.Close() - }() - var priv []byte - priv, err = ioutil.ReadAll(file) - if err != nil { - return err - } - privPem, _ := pem.Decode(priv) - switch privPem.Type { - case "RSA PRIVATE KEY": - if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privPem.Bytes); err != nil { - return err - } - case "EC PRIVATE KEY": - // https://tools.ietf.org/html/rfc5915 section 4. - if h.PrivateKey, err = x509.ParsePKCS8PrivateKey(privPem.Bytes); err != nil { - return err - } - default: - return fmt.Errorf("Key '%s' is not supported", privPem.Type) - } - return nil -} - // ServerVariable stores the information about a server variable type ServerVariable struct { Description string diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/docs/PetApi.md b/samples/openapi3/client/petstore/go-experimental/go-petstore/docs/PetApi.md index a3c4f9457760..f88a802be249 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/docs/PetApi.md +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/docs/PetApi.md @@ -41,7 +41,7 @@ Name | Type | Description | Notes ### Authorization -[petstore_auth](../README.md#petstore_auth) +[http_signature_test](../README.md#http_signature_test), [petstore_auth](../README.md#petstore_auth) ### HTTP request headers @@ -122,7 +122,7 @@ Name | Type | Description | Notes ### Authorization -[petstore_auth](../README.md#petstore_auth) +[http_signature_test](../README.md#http_signature_test), [petstore_auth](../README.md#petstore_auth) ### HTTP request headers @@ -161,7 +161,7 @@ Name | Type | Description | Notes ### Authorization -[petstore_auth](../README.md#petstore_auth) +[http_signature_test](../README.md#http_signature_test), [petstore_auth](../README.md#petstore_auth) ### HTTP request headers @@ -241,7 +241,7 @@ Name | Type | Description | Notes ### Authorization -[petstore_auth](../README.md#petstore_auth) +[http_signature_test](../README.md#http_signature_test), [petstore_auth](../README.md#petstore_auth) ### HTTP request headers From e8f7c4998fecf3ffda3b35ec36f74f0a63d4daf3 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Fri, 17 Jan 2020 16:02:43 -0800 Subject: [PATCH 36/48] move http signature code to separate file --- .../GoClientExperimentalCodegen.java | 1 + .../petstore/go-experimental/auth_test.go | 397 ------------------ 2 files changed, 1 insertion(+), 397 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoClientExperimentalCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoClientExperimentalCodegen.java index 289d1e45358e..4369c60ab99f 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoClientExperimentalCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoClientExperimentalCodegen.java @@ -77,6 +77,7 @@ public void processOpts() { List authMethods = fromSecurity(securitySchemeMap); if (ProcessUtils.hasHttpSignatureMethods(authMethods)) { supportingFiles.add(new SupportingFile("signing.mustache", "", "signing.go")); + supportingFiles.add(new SupportingFile("http_signature_test.mustache", "", "http_signature_test.go")); } } diff --git a/samples/client/petstore/go-experimental/auth_test.go b/samples/client/petstore/go-experimental/auth_test.go index 421b44c323ff..0e532aeff3ac 100644 --- a/samples/client/petstore/go-experimental/auth_test.go +++ b/samples/client/petstore/go-experimental/auth_test.go @@ -1,18 +1,9 @@ package main import ( - "bytes" "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "fmt" - "io/ioutil" "net/http" "net/http/httputil" - "os" - "regexp" "strings" "testing" "time" @@ -208,394 +199,6 @@ func TestAPIKeyWithPrefix(t *testing.T) { } } -// Test RSA private key as published in Appendix C 'Test Values' of -// https://www.ietf.org/id/draft-cavage-http-signatures-12.txt -const rsaTestPrivateKey string = `-----BEGIN RSA PRIVATE KEY----- -MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF -NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F -UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB -AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA -QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK -kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg -f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u -412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc -mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7 -kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA -gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW -G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI -7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== ------END RSA PRIVATE KEY-----` - -func writeTestRsaPemKey(t *testing.T, fileName string) { - err := ioutil.WriteFile(fileName, []byte(rsaTestPrivateKey), 0644) - if err != nil { - t.Fatalf("Error writing private key: %v", err) - } -} - -func writeRandomTestRsaPemKey(t *testing.T, fileName string) { - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatalf("Error generating RSA private key file: %v", err) - } - var outFile *os.File - outFile, err = os.Create(fileName) - if err != nil { - t.Fatalf("Error creating RSA private key file: %v", err) - } - defer outFile.Close() - - var privateKey = &pem.Block{ - Type: "PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(key), - } - - err = pem.Encode(outFile, privateKey) - if err != nil { - t.Fatalf("Error encoding RSA private key: %v", err) - } -} - -/* -Commented out because OpenAPITools is configured to use golang 1.8 at build time -x509.MarshalPKCS8PrivateKey is not present. -func writeRandomTestEcdsaPemKey(t *testing.T, fileName string) { - key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) - if err != nil { - t.Fatalf("Error generating ECDSA private key file: %v", err) - } - var outFile *os.File - outFile, err = os.Create(fileName) - if err != nil { - t.Fatalf("Error creating ECDSA private key file: %v", err) - } - defer outFile.Close() - - var keybytes []byte - keybytes, err = x509.MarshalPKCS8PrivateKey(key) - if err != nil { - t.Fatalf("Error marshaling ECDSA private key: %v", err) - } - var privateKey = &pem.Block{ - Type: "PRIVATE KEY", - Bytes: keybytes, - } - - err = pem.Encode(outFile, privateKey) - if err != nil { - t.Fatalf("Error encoding ECDSA private key: %v", err) - } -} -*/ - -func TestHttpSignaturePrivateKeys(t *testing.T) { - privateKeyPath := "privatekey.pem" - { - authConfig := sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: "hs2019", - SignedHeaders: []string{"Content-Type"}, - } - // Generate test private key. - writeRandomTestRsaPemKey(t, privateKeyPath) - err := authConfig.LoadPrivateKey(privateKeyPath) - if err != nil { - t.Fatalf("Error loading private key: %v", err) - } - } - /* - { - authConfig := sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: "hs2019", - SignedHeaders: []string{"Content-Type"}, - } - // Generate test private key. - writeRandomTestEcdsaPemKey(t, privateKeyPath) - err := authConfig.LoadPrivateKey(privateKeyPath) - if err != nil { - t.Fatalf("Error loading private key: %v", err) - } - } - */ -} - -func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, expectSuccess bool) string { - cfg := sw.NewConfiguration() - cfg.AddDefaultHeader("testheader", "testvalue") - cfg.AddDefaultHeader("Content-Type", "application/json") - cfg.Host = testHost - cfg.Scheme = testScheme - apiClient := sw.NewAPIClient(cfg) - - privateKeyPath := "rsa.pem" - writeTestRsaPemKey(t, privateKeyPath) - err := authConfig.LoadPrivateKey(privateKeyPath) - if err != nil { - t.Fatalf("Error loading private key: %v", err) - } - authCtx := context.WithValue(context.Background(), sw.ContextHttpSignatureAuth, *authConfig) - newPet := (sw.Pet{Id: sw.PtrInt64(12992), Name: "gopher", - PhotoUrls: []string{"http://1.com", "http://2.com"}, - Status: sw.PtrString("pending"), - Tags: &[]sw.Tag{sw.Tag{Id: sw.PtrInt64(1), Name: sw.PtrString("tag2")}}}) - - fmt.Printf("Request with HTTP signature. Scheme: '%s'. Algorithm: '%s'. MaxValidity: %v. Headers: '%v'\n", - authConfig.SigningScheme, authConfig.SigningAlgorithm, authConfig.SignatureMaxValidity, authConfig.SignedHeaders) - - r, err2 := apiClient.PetApi.AddPet(authCtx).Body(newPet).Execute() - if expectSuccess && err2 != nil { - t.Fatalf("Error while adding pet: %v", err2) - } - if !expectSuccess { - if err2 == nil { - t.Fatalf("Error was expected, but no error was generated") - } else { - // Do not continue. Error is expected. - return "" - } - } - if r.StatusCode != 200 { - t.Log(r) - } - - _, r, err = apiClient.PetApi.GetPetById(authCtx, 12992).Execute() - if expectSuccess && err != nil { - t.Fatalf("Error while deleting pet by id: %v", err) - } - - // The request should look like this: - // - // GET /v2/pet/12992 HTTP/1.1 - // Host: petstore.swagger.io:80 - // Accept: application/json - // Authorization: Signature keyId="my-key-id",algorithm="hs2019",created=1579033245,headers="(request-target) date host digest content-type",signature="RMJZjVVxzlH02wlxiQgUYDe4QxZaI5IJNIfB2BK8Dhbv3WQ2gw0xyqC+5HiKUmT/cfchhhkUNNsUtiVRnjZmFwtSfYxHfiQvH3KWXlLCMwKGNQC3YzD9lnoWdx0pA5Kxbr0/ygmr3+lTyuN2PJg4IS7Ji/AaKAqIZx7RsHS8Bxw=" - // Date: Tue, 14 Jan 2020 06:41:22 GMT - // Digest: SHA-512=z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg== - // Testheader: testvalue - // User-Agent: OpenAPI-Generator/1.0.0/go - reqb, _ := httputil.DumpRequest(r.Request, true) - reqt := (string)(reqb) - fmt.Printf("REQUEST:\n%v\n", reqt) - var sb bytes.Buffer - fmt.Fprintf(&sb, `Signature keyId="%s",algorithm="%s",`, - authConfig.KeyId, authConfig.SigningScheme) - if len(authConfig.SignedHeaders) == 0 { - fmt.Fprintf(&sb, `created=[0-9]+,`) - } else { - for _, header := range authConfig.SignedHeaders { - header = strings.ToLower(header) - if header == sw.HttpSignatureParameterCreated { - fmt.Fprintf(&sb, `created=[0-9]+,`) - } - if header == sw.HttpSignatureParameterExpires { - fmt.Fprintf(&sb, `expires=[0-9]+\.[0-9]{3},`) - } - } - } - fmt.Fprintf(&sb, `headers="`) - for i, header := range authConfig.SignedHeaders { - header = strings.ToLower(header) - if i > 0 { - fmt.Fprintf(&sb, " ") - } - fmt.Fprintf(&sb, regexp.QuoteMeta(header)) - switch header { - case "date": - if !strings.Contains(reqt, "Date: ") { - t.Errorf("Date header is incorrect") - } - case "digest": - var prefix string - switch authConfig.SigningScheme { - case sw.HttpSigningSchemeRsaSha256: - prefix = "SHA-256=" - default: - prefix = "SHA-512=" - } - if !strings.Contains(reqt, fmt.Sprintf("Digest: %s", prefix)) { - t.Errorf("Digest header is incorrect") - } - } - } - if len(authConfig.SignedHeaders) == 0 { - fmt.Fprintf(&sb, regexp.QuoteMeta(sw.HttpSignatureParameterCreated)) - } - fmt.Fprintf(&sb, `",signature="[a-zA-Z0-9+/]+="`) - re := regexp.MustCompile(sb.String()) - actual := r.Request.Header.Get("Authorization") - if !re.MatchString(actual) { - t.Errorf("Authorization header is incorrect. Expected regex\n'%s'\nbut got\n'%s'", sb.String(), actual) - } - return r.Request.Header.Get("Authorization") -} - -func TestHttpSignatureAuth(t *testing.T) { - // Test with 'hs2019' signature scheme, and default signature algorithm (RSA SSA PKCS1.5) - authConfig := sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, - SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. - sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. - "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. - "Date", // The date and time at which the message was originated. - "Content-Type", // The Media type of the body of the request. - "Digest", // A cryptographic digest of the request body. - }, - } - executeHttpSignatureAuth(t, &authConfig, true) - - // Test with duplicate headers. This is invalid and should be rejected. - authConfig = sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, - SignedHeaders: []string{"Host", "Date", "Host"}, - } - executeHttpSignatureAuth(t, &authConfig, false) - - // Test with non-existent header. This is invalid and should be rejected. - authConfig = sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, - SignedHeaders: []string{"Host", "Date", "Garbage-HeaderDoesNotExist"}, - } - executeHttpSignatureAuth(t, &authConfig, false) - - // Test with 'Authorization' header in the signed headers. This is invalid and should be rejected. - authConfig = sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, - SignedHeaders: []string{"Host", "Authorization"}, - } - executeHttpSignatureAuth(t, &authConfig, false) - - // Specify signature max validity, but '(expires)' parameter is missing. This should cause an error. - authConfig = sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, - SignatureMaxValidity: 7 * time.Minute, - } - executeHttpSignatureAuth(t, &authConfig, false) - - // Specify invalid signature max validity. This should cause an error. - authConfig = sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, - SignatureMaxValidity: -3 * time.Minute, - } - executeHttpSignatureAuth(t, &authConfig, false) - - // Specify signature max validity and '(expires)' parameter. - authConfig = sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, - SignatureMaxValidity: time.Minute, - SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. - sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. - sw.HttpSignatureParameterExpires, // Time when signature expires. - }, - } - executeHttpSignatureAuth(t, &authConfig, true) - - // Specify '(expires)' parameter but no signature max validity. - authConfig = sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, - SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. - sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. - sw.HttpSignatureParameterExpires, // Time when signature expires. - }, - } - executeHttpSignatureAuth(t, &authConfig, false) - - // Test with empty signed headers. The client should automatically add the "(created)" parameter by default. - authConfig = sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, - SignedHeaders: []string{}, - } - executeHttpSignatureAuth(t, &authConfig, true) - - // Test with deprecated RSA-SHA512, some servers may still support it. - authConfig = sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeRsaSha512, - SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. - sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. - "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. - "Date", // The date and time at which the message was originated. - "Content-Type", // The Media type of the body of the request. - "Digest", // A cryptographic digest of the request body. - }, - } - executeHttpSignatureAuth(t, &authConfig, true) - - // Test with deprecated RSA-SHA256, some servers may still support it. - authConfig = sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeRsaSha256, - SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. - sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. - "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. - "Date", // The date and time at which the message was originated. - "Content-Type", // The Media type of the body of the request. - "Digest", // A cryptographic digest of the request body. - }, - } - executeHttpSignatureAuth(t, &authConfig, true) - - // Test with headers without date. This makes it possible to get a fixed signature, used for unit test purpose. - // This should not be used in production code as it could potentially allow replay attacks. - authConfig = sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, - SigningAlgorithm: sw.HttpSigningAlgorithmRsaPKCS1v15, - SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, - "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. - "Content-Type", // The Media type of the body of the request. - "Digest", // A cryptographic digest of the request body. - }, - } - authorizationHeaderValue := executeHttpSignatureAuth(t, &authConfig, true) - expectedSignature := "sXE2MDeW8os6ywv1oUWaFEPFcSPCEb/msQ/NZGKNA9Emm/e42axaAPojzfkZ9Hacyw/iS/5nH4YIkczMgXu3z5fAwFjumxtf3OxbqvUcQ80wvw2/7B5aQmsF6ZwrCFHZ+L/cj9/bg7L1EGUGtdyDzoRKti4zf9QF/03OsP7QljI=" - expectedAuthorizationHeader := fmt.Sprintf( - `Signature keyId="my-key-id",`+ - `algorithm="hs2019",headers="(request-target) host content-type digest",`+ - `signature="%s"`, expectedSignature) - if authorizationHeaderValue != expectedAuthorizationHeader { - t.Errorf("Authorization header value is incorrect. Got\n'%s'\nbut expected\n'%s'", authorizationHeaderValue, expectedAuthorizationHeader) - } - - // Test with PSS signature. The PSS signature creates a new signature every time it is invoked. - authConfig = sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, - SigningAlgorithm: sw.HttpSigningAlgorithmRsaPSS, - SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, - "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. - "Content-Type", // The Media type of the body of the request. - "Digest", // A cryptographic digest of the request body. - }, - } - authorizationHeaderValue = executeHttpSignatureAuth(t, &authConfig, true) - expectedSignature = `[a-zA-Z0-9+/]+=` - expectedAuthorizationHeader = fmt.Sprintf( - `Signature keyId="my-key-id",`+ - `algorithm="hs2019",headers="\(request-target\) host content-type digest",`+ - `signature="%s"`, expectedSignature) - re := regexp.MustCompile(expectedAuthorizationHeader) - if !re.MatchString(authorizationHeaderValue) { - t.Errorf("Authorization header value is incorrect. Got\n'%s'\nbut expected\n'%s'", authorizationHeaderValue, expectedAuthorizationHeader) - } -} - func TestDefaultHeader(t *testing.T) { newPet := (sw.Pet{Id: sw.PtrInt64(12992), Name: "gopher", PhotoUrls: []string{"http://1.com", "http://2.com"}, Status: sw.PtrString("pending"), From bbbb5d62d048285aeaac2fe368c83d2260b4a455 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Fri, 17 Jan 2020 16:08:08 -0800 Subject: [PATCH 37/48] move http signature code to separate file --- .../http_signature_test.mustache | 407 ++++++++++++++++++ 1 file changed, 407 insertions(+) create mode 100644 modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache diff --git a/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache b/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache new file mode 100644 index 000000000000..c1fccaf6e8d2 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache @@ -0,0 +1,407 @@ +{{>partial_header}} +package {{packageName}} + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io/ioutil" + "net/http/httputil" + "os" + "regexp" + "strings" + "testing" + "time" +) + +// Test RSA private key as published in Appendix C 'Test Values' of +// https://www.ietf.org/id/draft-cavage-http-signatures-12.txt +const rsaTestPrivateKey string = `-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF +NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F +UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB +AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA +QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK +kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg +f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u +412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc +mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7 +kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA +gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW +G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI +7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== +-----END RSA PRIVATE KEY-----` + +func writeTestRsaPemKey(t *testing.T, fileName string) { + err := ioutil.WriteFile(fileName, []byte(rsaTestPrivateKey), 0644) + if err != nil { + t.Fatalf("Error writing private key: %v", err) + } +} + +func writeRandomTestRsaPemKey(t *testing.T, fileName string) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("Error generating RSA private key file: %v", err) + } + var outFile *os.File + outFile, err = os.Create(fileName) + if err != nil { + t.Fatalf("Error creating RSA private key file: %v", err) + } + defer outFile.Close() + + var privateKey = &pem.Block{ + Type: "PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + } + + err = pem.Encode(outFile, privateKey) + if err != nil { + t.Fatalf("Error encoding RSA private key: %v", err) + } +} + +/* +Commented out because OpenAPITools is configured to use golang 1.8 at build time +x509.MarshalPKCS8PrivateKey is not present. +func writeRandomTestEcdsaPemKey(t *testing.T, fileName string) { + key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + t.Fatalf("Error generating ECDSA private key file: %v", err) + } + var outFile *os.File + outFile, err = os.Create(fileName) + if err != nil { + t.Fatalf("Error creating ECDSA private key file: %v", err) + } + defer outFile.Close() + + var keybytes []byte + keybytes, err = x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatalf("Error marshaling ECDSA private key: %v", err) + } + var privateKey = &pem.Block{ + Type: "PRIVATE KEY", + Bytes: keybytes, + } + + err = pem.Encode(outFile, privateKey) + if err != nil { + t.Fatalf("Error encoding ECDSA private key: %v", err) + } +} +*/ + +func TestHttpSignaturePrivateKeys(t *testing.T) { + privateKeyPath := "privatekey.pem" + { + authConfig := sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: "hs2019", + SignedHeaders: []string{"Content-Type"}, + } + // Generate test private key. + writeRandomTestRsaPemKey(t, privateKeyPath) + err := authConfig.LoadPrivateKey(privateKeyPath) + if err != nil { + t.Fatalf("Error loading private key: %v", err) + } + } + /* + { + authConfig := sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: "hs2019", + SignedHeaders: []string{"Content-Type"}, + } + // Generate test private key. + writeRandomTestEcdsaPemKey(t, privateKeyPath) + err := authConfig.LoadPrivateKey(privateKeyPath) + if err != nil { + t.Fatalf("Error loading private key: %v", err) + } + } + */ +} + +func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, expectSuccess bool) string { + cfg := sw.NewConfiguration() + cfg.AddDefaultHeader("testheader", "testvalue") + cfg.AddDefaultHeader("Content-Type", "application/json") + cfg.Host = testHost + cfg.Scheme = testScheme + apiClient := sw.NewAPIClient(cfg) + + privateKeyPath := "rsa.pem" + writeTestRsaPemKey(t, privateKeyPath) + err := authConfig.LoadPrivateKey(privateKeyPath) + if err != nil { + t.Fatalf("Error loading private key: %v", err) + } + authCtx := context.WithValue(context.Background(), sw.ContextHttpSignatureAuth, *authConfig) + newPet := (sw.Pet{Id: sw.PtrInt64(12992), Name: "gopher", + PhotoUrls: []string{"http://1.com", "http://2.com"}, + Status: sw.PtrString("pending"), + Tags: &[]sw.Tag{sw.Tag{Id: sw.PtrInt64(1), Name: sw.PtrString("tag2")}}}) + + fmt.Printf("Request with HTTP signature. Scheme: '%s'. Algorithm: '%s'. MaxValidity: %v. Headers: '%v'\n", + authConfig.SigningScheme, authConfig.SigningAlgorithm, authConfig.SignatureMaxValidity, authConfig.SignedHeaders) + + r, err2 := apiClient.PetApi.AddPet(authCtx).Body(newPet).Execute() + if expectSuccess && err2 != nil { + t.Fatalf("Error while adding pet: %v", err2) + } + if !expectSuccess { + if err2 == nil { + t.Fatalf("Error was expected, but no error was generated") + } else { + // Do not continue. Error is expected. + return "" + } + } + if r.StatusCode != 200 { + t.Log(r) + } + + _, r, err = apiClient.PetApi.GetPetById(authCtx, 12992).Execute() + if expectSuccess && err != nil { + t.Fatalf("Error while deleting pet by id: %v", err) + } + + // The request should look like this: + // + // GET /v2/pet/12992 HTTP/1.1 + // Host: petstore.swagger.io:80 + // Accept: application/json + // Authorization: Signature keyId="my-key-id",algorithm="hs2019",created=1579033245,headers="(request-target) date host digest content-type",signature="RMJZjVVxzlH02wlxiQgUYDe4QxZaI5IJNIfB2BK8Dhbv3WQ2gw0xyqC+5HiKUmT/cfchhhkUNNsUtiVRnjZmFwtSfYxHfiQvH3KWXlLCMwKGNQC3YzD9lnoWdx0pA5Kxbr0/ygmr3+lTyuN2PJg4IS7Ji/AaKAqIZx7RsHS8Bxw=" + // Date: Tue, 14 Jan 2020 06:41:22 GMT + // Digest: SHA-512=z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg== + // Testheader: testvalue + // User-Agent: OpenAPI-Generator/1.0.0/go + reqb, _ := httputil.DumpRequest(r.Request, true) + reqt := (string)(reqb) + fmt.Printf("REQUEST:\n%v\n", reqt) + var sb bytes.Buffer + fmt.Fprintf(&sb, `Signature keyId="%s",algorithm="%s",`, + authConfig.KeyId, authConfig.SigningScheme) + if len(authConfig.SignedHeaders) == 0 { + fmt.Fprintf(&sb, `created=[0-9]+,`) + } else { + for _, header := range authConfig.SignedHeaders { + header = strings.ToLower(header) + if header == sw.HttpSignatureParameterCreated { + fmt.Fprintf(&sb, `created=[0-9]+,`) + } + if header == sw.HttpSignatureParameterExpires { + fmt.Fprintf(&sb, `expires=[0-9]+\.[0-9]{3},`) + } + } + } + fmt.Fprintf(&sb, `headers="`) + for i, header := range authConfig.SignedHeaders { + header = strings.ToLower(header) + if i > 0 { + fmt.Fprintf(&sb, " ") + } + fmt.Fprintf(&sb, regexp.QuoteMeta(header)) + switch header { + case "date": + if !strings.Contains(reqt, "Date: ") { + t.Errorf("Date header is incorrect") + } + case "digest": + var prefix string + switch authConfig.SigningScheme { + case sw.HttpSigningSchemeRsaSha256: + prefix = "SHA-256=" + default: + prefix = "SHA-512=" + } + if !strings.Contains(reqt, fmt.Sprintf("Digest: %s", prefix)) { + t.Errorf("Digest header is incorrect") + } + } + } + if len(authConfig.SignedHeaders) == 0 { + fmt.Fprintf(&sb, regexp.QuoteMeta(sw.HttpSignatureParameterCreated)) + } + fmt.Fprintf(&sb, `",signature="[a-zA-Z0-9+/]+="`) + re := regexp.MustCompile(sb.String()) + actual := r.Request.Header.Get("Authorization") + if !re.MatchString(actual) { + t.Errorf("Authorization header is incorrect. Expected regex\n'%s'\nbut got\n'%s'", sb.String(), actual) + } + return r.Request.Header.Get("Authorization") +} + +func TestHttpSignatureAuth(t *testing.T) { + // Test with 'hs2019' signature scheme, and default signature algorithm (RSA SSA PKCS1.5) + authConfig := sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Date", // The date and time at which the message was originated. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Test with duplicate headers. This is invalid and should be rejected. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{"Host", "Date", "Host"}, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Test with non-existent header. This is invalid and should be rejected. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{"Host", "Date", "Garbage-HeaderDoesNotExist"}, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Test with 'Authorization' header in the signed headers. This is invalid and should be rejected. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{"Host", "Authorization"}, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Specify signature max validity, but '(expires)' parameter is missing. This should cause an error. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignatureMaxValidity: 7 * time.Minute, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Specify invalid signature max validity. This should cause an error. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignatureMaxValidity: -3 * time.Minute, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Specify signature max validity and '(expires)' parameter. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignatureMaxValidity: time.Minute, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + sw.HttpSignatureParameterExpires, // Time when signature expires. + }, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Specify '(expires)' parameter but no signature max validity. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + sw.HttpSignatureParameterExpires, // Time when signature expires. + }, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Test with empty signed headers. The client should automatically add the "(created)" parameter by default. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{}, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Test with deprecated RSA-SHA512, some servers may still support it. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeRsaSha512, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Date", // The date and time at which the message was originated. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Test with deprecated RSA-SHA256, some servers may still support it. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeRsaSha256, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Date", // The date and time at which the message was originated. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Test with headers without date. This makes it possible to get a fixed signature, used for unit test purpose. + // This should not be used in production code as it could potentially allow replay attacks. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SigningAlgorithm: sw.HttpSigningAlgorithmRsaPKCS1v15, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + } + authorizationHeaderValue := executeHttpSignatureAuth(t, &authConfig, true) + expectedSignature := "sXE2MDeW8os6ywv1oUWaFEPFcSPCEb/msQ/NZGKNA9Emm/e42axaAPojzfkZ9Hacyw/iS/5nH4YIkczMgXu3z5fAwFjumxtf3OxbqvUcQ80wvw2/7B5aQmsF6ZwrCFHZ+L/cj9/bg7L1EGUGtdyDzoRKti4zf9QF/03OsP7QljI=" + expectedAuthorizationHeader := fmt.Sprintf( + `Signature keyId="my-key-id",`+ + `algorithm="hs2019",headers="(request-target) host content-type digest",`+ + `signature="%s"`, expectedSignature) + if authorizationHeaderValue != expectedAuthorizationHeader { + t.Errorf("Authorization header value is incorrect. Got\n'%s'\nbut expected\n'%s'", authorizationHeaderValue, expectedAuthorizationHeader) + } + + // Test with PSS signature. The PSS signature creates a new signature every time it is invoked. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SigningAlgorithm: sw.HttpSigningAlgorithmRsaPSS, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + } + authorizationHeaderValue = executeHttpSignatureAuth(t, &authConfig, true) + expectedSignature = `[a-zA-Z0-9+/]+=` + expectedAuthorizationHeader = fmt.Sprintf( + `Signature keyId="my-key-id",`+ + `algorithm="hs2019",headers="\(request-target\) host content-type digest",`+ + `signature="%s"`, expectedSignature) + re := regexp.MustCompile(expectedAuthorizationHeader) + if !re.MatchString(authorizationHeaderValue) { + t.Errorf("Authorization header value is incorrect. Got\n'%s'\nbut expected\n'%s'", authorizationHeaderValue, expectedAuthorizationHeader) + } +} From b4449295dc9ac6b7032a2b06f43a39bf1df5aadb Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Sat, 18 Jan 2020 23:09:23 -0800 Subject: [PATCH 38/48] Add passphrase to decrypt key --- .../go-experimental/signing.mustache | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache index 93dd76bd5151..31a220810483 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache @@ -76,9 +76,11 @@ const ( type HttpSignatureAuth struct { KeyId string // A key identifier. PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. + Passphrase string // The passphrase to decrypt the private key. SigningScheme string // The signature scheme, when signing HTTP requests. Supported value is 'hs2019'. - SigningAlgorithm string // The signature algorithm, when signing HTTP requests. - // Supported values are RSASSA-PKCS1-v1_5, RSASSA-PSS. + // The signature algorithm, when signing HTTP requests. + // Supported values are RSASSA-PKCS1-v1_5, RSASSA-PSS. + SigningAlgorithm string SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. // SignatureMaxValidity specifies the maximum duration of the signature validity. // The value is used to set the '(expires)' signature parameter in the HTTP request. @@ -102,19 +104,35 @@ func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { if err != nil { return err } - privPem, _ := pem.Decode(priv) - switch privPem.Type { + pemBlock, _ := pem.Decode(priv) + if pemBlock == nil { + // No PEM data has been found. + return fmt.Errorf("File '%s' does not contain PEM data", filename) + } + var privKey []byte + if x509.IsEncryptedPEMBlock(pemBlock) { + // The PEM data is encrypted. + privKey, err = x509.DecryptPEMBlock(pemBlock, h.Passphrase) + if err != nil { + // Failed to decrypt PEM block. Because of deficiencies in the encrypted-PEM format, + // it's not always possibleto detect an incorrect password. + return err + } + } else { + privKey = pemBlock.Bytes + } + switch pemBlock.Type { case "RSA PRIVATE KEY": - if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privPem.Bytes); err != nil { + if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privKey); err != nil { return err } - case "EC PRIVATE KEY": + case "EC PRIVATE KEY", "PRIVATE KEY": // https://tools.ietf.org/html/rfc5915 section 4. - if h.PrivateKey, err = x509.ParsePKCS8PrivateKey(privPem.Bytes); err != nil { + if h.PrivateKey, err = x509.ParsePKCS8PrivateKey(privKey); err != nil { return err } default: - return fmt.Errorf("Key '%s' is not supported", privPem.Type) + return fmt.Errorf("Key '%s' is not supported", pemBlock.Type) } return nil } From d5d255232774ebc6ab5f4eef4fb32ec40291ea51 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Sun, 19 Jan 2020 17:25:08 -0800 Subject: [PATCH 39/48] Add more unit tests --- bin/openapi3/go-experimental-petstore.sh | 3 + .../http_signature_test.mustache | 98 ++++++++++++++++--- .../go-experimental/signing.mustache | 5 +- 3 files changed, 91 insertions(+), 15 deletions(-) diff --git a/bin/openapi3/go-experimental-petstore.sh b/bin/openapi3/go-experimental-petstore.sh index c6b30c5c544d..53d3e78cca65 100755 --- a/bin/openapi3/go-experimental-petstore.sh +++ b/bin/openapi3/go-experimental-petstore.sh @@ -26,6 +26,9 @@ then fi #SPEC="modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml" +# petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml is the same as the above file, with +# the addition of the HTTP signature security scheme. Ideally, this would have been directly added to +# petstore-with-fake-endpoints-models-for-testing.yaml, but this cannot be done until issue #5025 is resolved. SPEC="modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml" GENERATOR="go-experimental" STUB_DIR="samples/openapi3/client/petstore/go-experimental/go-petstore" diff --git a/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache b/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache index c1fccaf6e8d2..13fbf3d53de2 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache @@ -43,8 +43,16 @@ func writeTestRsaPemKey(t *testing.T, fileName string) { } } -func writeRandomTestRsaPemKey(t *testing.T, fileName string) { - key, err := rsa.GenerateKey(rand.Reader, 2048) +type keyFormat int // The serialization format of the private key. + +const ( + keyFormatPem keyFormat = iota // Private key is serialized in PEM format. + keyFormatPkcs8Pem // Private key is serialized as PKCS#8 encoded in PEM format. + keyFormatPkcs8Der // Private key is serialized as PKCS#8 encoded in DER format. +) + +func writeRandomTestRsaPemKey(t *testing.T, fileName string, bits int, format keyFormat, passphrase string, alg *x509.PEMCipher) { + key, err := rsa.GenerateKey(rand.Reader, bits) if err != nil { t.Fatalf("Error generating RSA private key file: %v", err) } @@ -54,13 +62,41 @@ func writeRandomTestRsaPemKey(t *testing.T, fileName string) { t.Fatalf("Error creating RSA private key file: %v", err) } defer outFile.Close() - - var privateKey = &pem.Block{ - Type: "PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(key), + var privKeyBytes []byte + switch format { + case keyFormatPem: + if passphrase != "" { + f.Fatalf("Encrypting PKCS#1-encoded private key with passphrase is not supported") + } + privKeyBytes = x509.MarshalPKCS1PrivateKey(key) + case keyFormatPkcs8Pem: + privKeyBytes = x509.MarshalPKCS8PrivateKey(key) + case keyFormatPkcs8Der: + if passphrase != "" { + f.Fatalf("Encrypting DER-encoded private key with passphrase is not supported") + } + _, err = outFile.Write(x509.MarshalPKCS8PrivateKey(key)) + if err != nil { + t.Fatalf("Error writing RSA private key: %v", err) + } + return + default: + t.Fatalf("Unsupported key format: %v", format) } - err = pem.Encode(outFile, privateKey) + var pemBlock *pem.Block + if passphrase == "" { + pemBlock = &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privKeyBytes, + } + } else { + pemBlock, err = x509.EncryptPEMBlock(rand.Reader, "ENCRYPTED PRIVATE KEY", privKeyBytes, passphrase, *alg) + if err != nil { + t.Fatalf("Error encoding RSA private key: %v", err) + } + } + err = pem.Encode(outFile, pemBlock) if err != nil { t.Fatalf("Error encoding RSA private key: %v", err) } @@ -98,23 +134,57 @@ func writeRandomTestEcdsaPemKey(t *testing.T, fileName string) { } */ +// TestHttpSignaturePrivateKeys creates private keys of various sizes, serialization format, +// clear-text and password encrypted. +// Test unmarshaling of the private key. func TestHttpSignaturePrivateKeys(t *testing.T) { - privateKeyPath := "privatekey.pem" - { + pemCiphers := []x509.PEMCipher{ + x509.PEMCipherDES, + x509.PEMCipher3DES, + x509.PEMCipherAES128, + x509.PEMCipherAES192, + x509.PEMCipherAES256, + } + // Test RSA private keys with various key lengths. + for _, bits := range []int{1024, 2048, 3072, 4096} { + + // Generate keys in PEM format. authConfig := sw.HttpSignatureAuth{ KeyId: "my-key-id", SigningScheme: "hs2019", SignedHeaders: []string{"Content-Type"}, } - // Generate test private key. - writeRandomTestRsaPemKey(t, privateKeyPath) - err := authConfig.LoadPrivateKey(privateKeyPath) - if err != nil { - t.Fatalf("Error loading private key: %v", err) + + for _, format := range []keyFormat{keyFormatPem, keyFormatPkcs8Pem, keyFormatPkcs8Der} { + // Generate test private key. + var privateKeyPath string + switch format { + case keyFormatPem, keyFormatPkcs8Pem: + privateKeyPath = "privatekey.pem" + case keyFormatPkcs8Der: + privateKeyPath = "privatekey.der" + } + writeRandomTestRsaPemKey(t, privateKeyPath, bits, format, "", nil) + err := authConfig.LoadPrivateKey(privateKeyPath) + if err != nil { + t.Fatalf("Error loading private key: %v", err) + } + + for _, alg := range pemCiphers { + writeRandomTestRsaPemKey(t, privateKeyPath, bits, format, passphrase, alg) + err := authConfig.LoadPrivateKey(privateKeyPath) + if err != nil { + t.Fatalf("Error loading private key: %v", err) + } + } } } + /* + Unfortunately, currently the build environment for OpenAPITools is using an old version + of golang that does not support ECDSA keys. { + privateKeyPath := "privatekey.pem" authConfig := sw.HttpSignatureAuth{ KeyId: "my-key-id", SigningScheme: "hs2019", diff --git a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache index 31a220810483..6221aff9ce17 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache @@ -76,7 +76,7 @@ const ( type HttpSignatureAuth struct { KeyId string // A key identifier. PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. - Passphrase string // The passphrase to decrypt the private key. + Passphrase string // The passphrase to decrypt the private key, if the key is encrypted. SigningScheme string // The signature scheme, when signing HTTP requests. Supported value is 'hs2019'. // The signature algorithm, when signing HTTP requests. // Supported values are RSASSA-PKCS1-v1_5, RSASSA-PSS. @@ -104,6 +104,9 @@ func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { if err != nil { return err } + if !strings.HasSuffix(filename, ".pem") { + return fmt.Errorf("Private key must be in PEM format.") + } pemBlock, _ := pem.Decode(priv) if pemBlock == nil { // No PEM data has been found. From 34f126fb65077ecf2d23d12907158471c37fc93c Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Sun, 19 Jan 2020 21:13:01 -0800 Subject: [PATCH 40/48] do not throw exception if security scheme is unrecognized --- .../src/main/java/org/openapitools/codegen/DefaultCodegen.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 06a286f24f07..acf263be0329 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -3646,8 +3646,6 @@ public List fromSecurity(Map securitySc // As of January 2020, the "signature" scheme has not been registered with IANA yet. // This scheme may have to be changed when it is officially registered with IANA. cs.isHttpSignature = true; - } else { - throw new RuntimeException("Unsupported security scheme: " + securityScheme.getScheme()); } } else if (SecurityScheme.Type.OAUTH2.equals(securityScheme.getType())) { cs.isKeyInHeader = cs.isKeyInQuery = cs.isKeyInCookie = cs.isApiKey = cs.isBasic = false; From 23746294b55381b7a4c42fcd71acdc18d61a3409 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Tue, 21 Jan 2020 14:31:36 -0800 Subject: [PATCH 41/48] Include HTTP signature in README file --- .../resources/go-experimental/README.mustache | 36 ++++++++++- .../go-experimental/signing.mustache | 59 +++++++++++++------ .../go-experimental/go-petstore/README.md | 43 ++++++++------ 3 files changed, 100 insertions(+), 38 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/README.mustache b/modules/openapi-generator/src/main/resources/go-experimental/README.mustache index cdb72d59099e..635014c01909 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/README.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/README.mustache @@ -105,7 +105,9 @@ Class | Method | HTTP request | Description Note, each API key must be added to a map of `map[string]APIKey` where the key is: {{keyParamName}} and passed in as the auth context for each request. {{/isApiKey}} -{{#isBasic}}- **Type**: HTTP basic authentication +{{#isBasic}} +{{#isBasicBasic}} +- **Type**: HTTP basic authentication Example @@ -117,6 +119,38 @@ auth := context.WithValue(context.Background(), sw.ContextBasicAuth, sw.BasicAut r, err := client.Service.Operation(auth, args) ``` +{{/isBasicBasic}} +{{#isHttpSignature}} +- **Type**: HTTP signature authentication + +Example + +```golang + authConfig := sw.HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: "rsa.pem", + Passphrase: "my-passphrase", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Date", // The date and time at which the message was originated. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + SigningAlgorithm: sw.HttpSigningAlgorithmRsaPSS, + SignatureMaxValidity: 5 * time.Minute, + } + var authCtx context.Context + var err error + if authCtx, err = authConfig.ContextWithValue(context.Background()); err != nil { + // Process error + } + r, err = client.Service.Operation(auth, args) + +``` +{{/isHttpSignature}} {{/isBasic}} {{#isOAuth}} diff --git a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache index 6221aff9ce17..d95359aa8fe0 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache @@ -26,9 +26,21 @@ const ( // The '(expires)' parameter expresses when the signature ceases to // be valid. The value MUST be a Unix timestamp integer value. HttpSignatureParameterExpires string = "(expires)" - // The HTTP Authorization header, as specified in RFC 7235, Section 4.2. - HttpSignatureParameterAuthorization string = "authorization" +) +const ( + // Constants for HTTP headers. + // The 'Host' header, as defined in RFC 2616, section 14.23. + HttpHeaderHost string = "Host" + // The 'Date' header. + HttpHeaderDate string = "Date" + // The digest header, as defined in RFC 3230, section 4.3.2. + HttpHeaderDigest string = "Digest" + // The HTTP Authorization header, as defined in RFC 7235, section 4.2. + HttpHeaderAuthorization string = "Authorization" +) + +const ( // Specifies the Digital Signature Algorithm is derived from metadata // associated with 'keyId'. Supported DSA algorithms are RSASSA-PKCS1-v1_5, // RSASSA-PSS, and ECDSA. @@ -75,7 +87,7 @@ const ( // 3. Include the 'Digest' header and value in the HTTP signature. type HttpSignatureAuth struct { KeyId string // A key identifier. - PrivateKey crypto.PrivateKey // The private key used to sign HTTP requests. + PrivateKeyPath string // The path to the private key. Passphrase string // The passphrase to decrypt the private key, if the key is encrypted. SigningScheme string // The signature scheme, when signing HTTP requests. Supported value is 'hs2019'. // The signature algorithm, when signing HTTP requests. @@ -87,12 +99,23 @@ type HttpSignatureAuth struct { // '(expires)' is set to '(created)' plus the value of the SignatureMaxValidity field. // To specify the '(expires)' signature parameter, set 'SignatureMaxValidity' and add '(expires)' to 'SignedHeaders'. SignatureMaxValidity time.Duration + privateKey crypto.PrivateKey // The private key used to sign HTTP requests. } -// LoadPrivateKey reads the private key from the specified file. -func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { +// ContextWithValue validates the HttpSignatureAuth configuration parameters and returns a context +// suitable for HTTP signature. An error is returned if the HttpSignatureAuth configuration parameters +// are invalid. +func (h *HttpSignatureAuth) ContextWithValue(ctx context.Context) (context.Context, error) { + if err := h.loadPrivateKey(); err != nil { + return err + } + return context.WithValue(ctx, ContextHttpSignatureAuth, *h), nil +} + +// loadPrivateKey reads the private key from the file specified in the HttpSignatureAuth. +func (h *HttpSignatureAuth) loadPrivateKey() (err error) { var file *os.File - file, err = os.Open(filename) + file, err = os.Open(h.PrivateKeyPath) if err != nil { return err } @@ -104,13 +127,13 @@ func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { if err != nil { return err } - if !strings.HasSuffix(filename, ".pem") { + if !strings.HasSuffix(h.PrivateKeyPath, ".pem") { return fmt.Errorf("Private key must be in PEM format.") } pemBlock, _ := pem.Decode(priv) if pemBlock == nil { // No PEM data has been found. - return fmt.Errorf("File '%s' does not contain PEM data", filename) + return fmt.Errorf("File '%s' does not contain PEM data", h.PrivateKeyPath) } var privKey []byte if x509.IsEncryptedPEMBlock(pemBlock) { @@ -126,12 +149,12 @@ func (h *HttpSignatureAuth) LoadPrivateKey(filename string) (err error) { } switch pemBlock.Type { case "RSA PRIVATE KEY": - if h.PrivateKey, err = x509.ParsePKCS1PrivateKey(privKey); err != nil { + if h.privateKey, err = x509.ParsePKCS1PrivateKey(privKey); err != nil { return err } case "EC PRIVATE KEY", "PRIVATE KEY": // https://tools.ietf.org/html/rfc5915 section 4. - if h.PrivateKey, err = x509.ParsePKCS8PrivateKey(privKey); err != nil { + if h.privateKey, err = x509.ParsePKCS8PrivateKey(privKey); err != nil { return err } default: @@ -157,7 +180,7 @@ func SignRequest( r *http.Request, auth HttpSignatureAuth) error { - if auth.PrivateKey == nil { + if auth.privateKey == nil { return errors.New("Private key is not set") } now := time.Now() @@ -226,7 +249,7 @@ func SignRequest( header = strings.ToLower(header) var value string switch header { - case HttpSignatureParameterAuthorization: + case strings.ToLower(HttpHeaderAuthorization): return fmt.Errorf("Cannot include the 'Authorization' header as a signed header.") case HttpSignatureParameterRequestTarget: value = requestTarget @@ -241,7 +264,7 @@ func SignRequest( hasExpiresParameter = true case "date": value = date - r.Header.Set("Date", date) + r.Header.Set(HttpHeaderDate, date) case "digest": // Calculate the digest of the HTTP request body. // Calculate body digest per RFC 3230 section 4.3.2 @@ -260,10 +283,10 @@ func SignRequest( } d := bodyHash.Sum(nil) value = prefix + base64.StdEncoding.EncodeToString(d) - r.Header.Set("Digest", value) + r.Header.Set(HttpHeaderDigest, value) case "host": value = r.Host - r.Header.Set("Host", r.Host) + r.Header.Set(HttpHeaderHost, r.Host) default: var ok bool var v []string @@ -296,7 +319,7 @@ func SignRequest( d := msgHash.Sum(nil) var signature []byte - switch key := auth.PrivateKey.(type) { + switch key := auth.privateKey.(type) { case *rsa.PrivateKey: switch auth.SigningAlgorithm { case HttpSigningAlgorithmRsaPKCS1v15: @@ -335,6 +358,6 @@ func SignRequest( } fmt.Fprintf(&sb, `headers="%s",signature="%s"`, headers_list, base64.StdEncoding.EncodeToString(signature)) authStr := sb.String() - r.Header.Set("Authorization", authStr) + r.Header.Set(HttpHeaderAuthorization, authStr) return nil -} \ No newline at end of file +} diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md b/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md index 77a389bf6ad5..c0aad06ce222 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md @@ -190,18 +190,6 @@ Note, each API key must be added to a map of `map[string]APIKey` where the key i ### bearer_test -- **Type**: HTTP basic authentication - -Example - -```golang -auth := context.WithValue(context.Background(), sw.ContextBasicAuth, sw.BasicAuth{ - UserName: "username", - Password: "password", -}) -r, err := client.Service.Operation(auth, args) -``` - ### http_basic_test @@ -220,18 +208,35 @@ r, err := client.Service.Operation(auth, args) ### http_signature_test -- **Type**: HTTP basic authentication +- **Type**: HTTP signature authentication Example ```golang -auth := context.WithValue(context.Background(), sw.ContextBasicAuth, sw.BasicAuth{ - UserName: "username", - Password: "password", -}) -r, err := client.Service.Operation(auth, args) -``` + authConfig := sw.HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: "rsa.pem", + Passphrase: "my-passphrase", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Date", // The date and time at which the message was originated. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + SigningAlgorithm: sw.HttpSigningAlgorithmRsaPSS, + SignatureMaxValidity: 5 * time.Minute, + } + var authCtx context.Context + var err error + if authCtx, err = authConfig.ContextWithValue(context.Background()); err != nil { + // Process error + } + r, err = client.Service.Operation(auth, args) +``` ### petstore_auth From 743aff2f276d0aa977470c44083d8872b6ba8008 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Tue, 21 Jan 2020 14:49:14 -0800 Subject: [PATCH 42/48] Add generated files for HTTP signature --- .../go-experimental/go-petstore/api_fake.go | 32 +- .../go-petstore/http_signature_test.go | 485 ++++++++++++++++++ .../go-experimental/go-petstore/signing.go | 371 ++++++++++++++ 3 files changed, 874 insertions(+), 14 deletions(-) create mode 100644 samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go create mode 100644 samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/api_fake.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/api_fake.go index 11699dfe9af7..c0bf58b737af 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/api_fake.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/api_fake.go @@ -1767,26 +1767,30 @@ func (r apiTestQueryParameterCollectionFormatRequest) Execute() (*_nethttp.Respo return nil, reportError("context is required and must be specified") } - t := *r.pipe - if reflect.TypeOf(t).Kind() == reflect.Slice { - s := reflect.ValueOf(t) - for i := 0; i < s.Len(); i++ { - localVarQueryParams.Add("pipe", parameterToString(s.Index(i), "multi")) + { + t := *r.pipe + if reflect.TypeOf(t).Kind() == reflect.Slice { + s := reflect.ValueOf(t) + for i := 0; i < s.Len(); i++ { + localVarQueryParams.Add("pipe", parameterToString(s.Index(i), "multi")) + } + } else { + localVarQueryParams.Add("pipe", parameterToString(t, "multi")) } - } else { - localVarQueryParams.Add("pipe", parameterToString(t, "multi")) } localVarQueryParams.Add("ioutil", parameterToString(*r.ioutil, "csv")) localVarQueryParams.Add("http", parameterToString(*r.http, "space")) localVarQueryParams.Add("url", parameterToString(*r.url, "csv")) - t := *r.context - if reflect.TypeOf(t).Kind() == reflect.Slice { - s := reflect.ValueOf(t) - for i := 0; i < s.Len(); i++ { - localVarQueryParams.Add("context", parameterToString(s.Index(i), "multi")) + { + t := *r.context + if reflect.TypeOf(t).Kind() == reflect.Slice { + s := reflect.ValueOf(t) + for i := 0; i < s.Len(); i++ { + localVarQueryParams.Add("context", parameterToString(s.Index(i), "multi")) + } + } else { + localVarQueryParams.Add("context", parameterToString(t, "multi")) } - } else { - localVarQueryParams.Add("context", parameterToString(t, "multi")) } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go new file mode 100644 index 000000000000..2623c0ab4902 --- /dev/null +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go @@ -0,0 +1,485 @@ +/* + * OpenAPI Petstore + * + * This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\ + * + * API version: 1.0.0 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io/ioutil" + "net/http/httputil" + "os" + "regexp" + "strings" + "testing" + "time" +) + +// Test RSA private key as published in Appendix C 'Test Values' of +// https://www.ietf.org/id/draft-cavage-http-signatures-12.txt +const rsaTestPrivateKey string = `-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF +NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F +UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB +AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA +QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK +kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg +f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u +412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc +mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7 +kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA +gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW +G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI +7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== +-----END RSA PRIVATE KEY-----` + +func writeTestRsaPemKey(t *testing.T, fileName string) { + err := ioutil.WriteFile(fileName, []byte(rsaTestPrivateKey), 0644) + if err != nil { + t.Fatalf("Error writing private key: %v", err) + } +} + +type keyFormat int // The serialization format of the private key. + +const ( + keyFormatPem keyFormat = iota // Private key is serialized in PEM format. + keyFormatPkcs8Pem // Private key is serialized as PKCS#8 encoded in PEM format. + keyFormatPkcs8Der // Private key is serialized as PKCS#8 encoded in DER format. +) + +func writeRandomTestRsaPemKey(t *testing.T, fileName string, bits int, format keyFormat, passphrase string, alg *x509.PEMCipher) { + key, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + t.Fatalf("Error generating RSA private key file: %v", err) + } + var outFile *os.File + outFile, err = os.Create(fileName) + if err != nil { + t.Fatalf("Error creating RSA private key file: %v", err) + } + defer outFile.Close() + var privKeyBytes []byte + switch format { + case keyFormatPem: + if passphrase != "" { + f.Fatalf("Encrypting PKCS#1-encoded private key with passphrase is not supported") + } + privKeyBytes = x509.MarshalPKCS1PrivateKey(key) + case keyFormatPkcs8Pem: + privKeyBytes = x509.MarshalPKCS8PrivateKey(key) + case keyFormatPkcs8Der: + if passphrase != "" { + f.Fatalf("Encrypting DER-encoded private key with passphrase is not supported") + } + _, err = outFile.Write(x509.MarshalPKCS8PrivateKey(key)) + if err != nil { + t.Fatalf("Error writing RSA private key: %v", err) + } + return + default: + t.Fatalf("Unsupported key format: %v", format) + } + + var pemBlock *pem.Block + if passphrase == "" { + pemBlock = &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privKeyBytes, + } + } else { + pemBlock, err = x509.EncryptPEMBlock(rand.Reader, "ENCRYPTED PRIVATE KEY", privKeyBytes, passphrase, *alg) + if err != nil { + t.Fatalf("Error encoding RSA private key: %v", err) + } + } + err = pem.Encode(outFile, pemBlock) + if err != nil { + t.Fatalf("Error encoding RSA private key: %v", err) + } +} + +/* +Commented out because OpenAPITools is configured to use golang 1.8 at build time +x509.MarshalPKCS8PrivateKey is not present. +func writeRandomTestEcdsaPemKey(t *testing.T, fileName string) { + key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + t.Fatalf("Error generating ECDSA private key file: %v", err) + } + var outFile *os.File + outFile, err = os.Create(fileName) + if err != nil { + t.Fatalf("Error creating ECDSA private key file: %v", err) + } + defer outFile.Close() + + var keybytes []byte + keybytes, err = x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatalf("Error marshaling ECDSA private key: %v", err) + } + var privateKey = &pem.Block{ + Type: "PRIVATE KEY", + Bytes: keybytes, + } + + err = pem.Encode(outFile, privateKey) + if err != nil { + t.Fatalf("Error encoding ECDSA private key: %v", err) + } +} +*/ + +// TestHttpSignaturePrivateKeys creates private keys of various sizes, serialization format, +// clear-text and password encrypted. +// Test unmarshaling of the private key. +func TestHttpSignaturePrivateKeys(t *testing.T) { + pemCiphers := []x509.PEMCipher{ + x509.PEMCipherDES, + x509.PEMCipher3DES, + x509.PEMCipherAES128, + x509.PEMCipherAES192, + x509.PEMCipherAES256, + } + // Test RSA private keys with various key lengths. + for _, bits := range []int{1024, 2048, 3072, 4096} { + + // Generate keys in PEM format. + authConfig := sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: "hs2019", + SignedHeaders: []string{"Content-Type"}, + } + + for _, format := range []keyFormat{keyFormatPem, keyFormatPkcs8Pem, keyFormatPkcs8Der} { + // Generate test private key. + var privateKeyPath string + switch format { + case keyFormatPem, keyFormatPkcs8Pem: + privateKeyPath = "privatekey.pem" + case keyFormatPkcs8Der: + privateKeyPath = "privatekey.der" + } + writeRandomTestRsaPemKey(t, privateKeyPath, bits, format, "", nil) + err := authConfig.LoadPrivateKey(privateKeyPath) + if err != nil { + t.Fatalf("Error loading private key: %v", err) + } + + for _, alg := range pemCiphers { + writeRandomTestRsaPemKey(t, privateKeyPath, bits, format, passphrase, alg) + err := authConfig.LoadPrivateKey(privateKeyPath) + if err != nil { + t.Fatalf("Error loading private key: %v", err) + } + } + } + } + + /* + Unfortunately, currently the build environment for OpenAPITools is using an old version + of golang that does not support ECDSA keys. + { + privateKeyPath := "privatekey.pem" + authConfig := sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: "hs2019", + SignedHeaders: []string{"Content-Type"}, + } + // Generate test private key. + writeRandomTestEcdsaPemKey(t, privateKeyPath) + err := authConfig.LoadPrivateKey(privateKeyPath) + if err != nil { + t.Fatalf("Error loading private key: %v", err) + } + } + */ +} + +func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, expectSuccess bool) string { + cfg := sw.NewConfiguration() + cfg.AddDefaultHeader("testheader", "testvalue") + cfg.AddDefaultHeader("Content-Type", "application/json") + cfg.Host = testHost + cfg.Scheme = testScheme + apiClient := sw.NewAPIClient(cfg) + + privateKeyPath := "rsa.pem" + writeTestRsaPemKey(t, privateKeyPath) + err := authConfig.LoadPrivateKey(privateKeyPath) + if err != nil { + t.Fatalf("Error loading private key: %v", err) + } + authCtx := context.WithValue(context.Background(), sw.ContextHttpSignatureAuth, *authConfig) + newPet := (sw.Pet{Id: sw.PtrInt64(12992), Name: "gopher", + PhotoUrls: []string{"http://1.com", "http://2.com"}, + Status: sw.PtrString("pending"), + Tags: &[]sw.Tag{sw.Tag{Id: sw.PtrInt64(1), Name: sw.PtrString("tag2")}}}) + + fmt.Printf("Request with HTTP signature. Scheme: '%s'. Algorithm: '%s'. MaxValidity: %v. Headers: '%v'\n", + authConfig.SigningScheme, authConfig.SigningAlgorithm, authConfig.SignatureMaxValidity, authConfig.SignedHeaders) + + r, err2 := apiClient.PetApi.AddPet(authCtx).Body(newPet).Execute() + if expectSuccess && err2 != nil { + t.Fatalf("Error while adding pet: %v", err2) + } + if !expectSuccess { + if err2 == nil { + t.Fatalf("Error was expected, but no error was generated") + } else { + // Do not continue. Error is expected. + return "" + } + } + if r.StatusCode != 200 { + t.Log(r) + } + + _, r, err = apiClient.PetApi.GetPetById(authCtx, 12992).Execute() + if expectSuccess && err != nil { + t.Fatalf("Error while deleting pet by id: %v", err) + } + + // The request should look like this: + // + // GET /v2/pet/12992 HTTP/1.1 + // Host: petstore.swagger.io:80 + // Accept: application/json + // Authorization: Signature keyId="my-key-id",algorithm="hs2019",created=1579033245,headers="(request-target) date host digest content-type",signature="RMJZjVVxzlH02wlxiQgUYDe4QxZaI5IJNIfB2BK8Dhbv3WQ2gw0xyqC+5HiKUmT/cfchhhkUNNsUtiVRnjZmFwtSfYxHfiQvH3KWXlLCMwKGNQC3YzD9lnoWdx0pA5Kxbr0/ygmr3+lTyuN2PJg4IS7Ji/AaKAqIZx7RsHS8Bxw=" + // Date: Tue, 14 Jan 2020 06:41:22 GMT + // Digest: SHA-512=z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg== + // Testheader: testvalue + // User-Agent: OpenAPI-Generator/1.0.0/go + reqb, _ := httputil.DumpRequest(r.Request, true) + reqt := (string)(reqb) + fmt.Printf("REQUEST:\n%v\n", reqt) + var sb bytes.Buffer + fmt.Fprintf(&sb, `Signature keyId="%s",algorithm="%s",`, + authConfig.KeyId, authConfig.SigningScheme) + if len(authConfig.SignedHeaders) == 0 { + fmt.Fprintf(&sb, `created=[0-9]+,`) + } else { + for _, header := range authConfig.SignedHeaders { + header = strings.ToLower(header) + if header == sw.HttpSignatureParameterCreated { + fmt.Fprintf(&sb, `created=[0-9]+,`) + } + if header == sw.HttpSignatureParameterExpires { + fmt.Fprintf(&sb, `expires=[0-9]+\.[0-9]{3},`) + } + } + } + fmt.Fprintf(&sb, `headers="`) + for i, header := range authConfig.SignedHeaders { + header = strings.ToLower(header) + if i > 0 { + fmt.Fprintf(&sb, " ") + } + fmt.Fprintf(&sb, regexp.QuoteMeta(header)) + switch header { + case "date": + if !strings.Contains(reqt, "Date: ") { + t.Errorf("Date header is incorrect") + } + case "digest": + var prefix string + switch authConfig.SigningScheme { + case sw.HttpSigningSchemeRsaSha256: + prefix = "SHA-256=" + default: + prefix = "SHA-512=" + } + if !strings.Contains(reqt, fmt.Sprintf("Digest: %s", prefix)) { + t.Errorf("Digest header is incorrect") + } + } + } + if len(authConfig.SignedHeaders) == 0 { + fmt.Fprintf(&sb, regexp.QuoteMeta(sw.HttpSignatureParameterCreated)) + } + fmt.Fprintf(&sb, `",signature="[a-zA-Z0-9+/]+="`) + re := regexp.MustCompile(sb.String()) + actual := r.Request.Header.Get("Authorization") + if !re.MatchString(actual) { + t.Errorf("Authorization header is incorrect. Expected regex\n'%s'\nbut got\n'%s'", sb.String(), actual) + } + return r.Request.Header.Get("Authorization") +} + +func TestHttpSignatureAuth(t *testing.T) { + // Test with 'hs2019' signature scheme, and default signature algorithm (RSA SSA PKCS1.5) + authConfig := sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Date", // The date and time at which the message was originated. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Test with duplicate headers. This is invalid and should be rejected. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{"Host", "Date", "Host"}, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Test with non-existent header. This is invalid and should be rejected. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{"Host", "Date", "Garbage-HeaderDoesNotExist"}, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Test with 'Authorization' header in the signed headers. This is invalid and should be rejected. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{"Host", "Authorization"}, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Specify signature max validity, but '(expires)' parameter is missing. This should cause an error. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignatureMaxValidity: 7 * time.Minute, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Specify invalid signature max validity. This should cause an error. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignatureMaxValidity: -3 * time.Minute, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Specify signature max validity and '(expires)' parameter. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignatureMaxValidity: time.Minute, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + sw.HttpSignatureParameterExpires, // Time when signature expires. + }, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Specify '(expires)' parameter but no signature max validity. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + sw.HttpSignatureParameterExpires, // Time when signature expires. + }, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Test with empty signed headers. The client should automatically add the "(created)" parameter by default. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{}, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Test with deprecated RSA-SHA512, some servers may still support it. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeRsaSha512, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Date", // The date and time at which the message was originated. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Test with deprecated RSA-SHA256, some servers may still support it. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeRsaSha256, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Date", // The date and time at which the message was originated. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Test with headers without date. This makes it possible to get a fixed signature, used for unit test purpose. + // This should not be used in production code as it could potentially allow replay attacks. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SigningAlgorithm: sw.HttpSigningAlgorithmRsaPKCS1v15, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + } + authorizationHeaderValue := executeHttpSignatureAuth(t, &authConfig, true) + expectedSignature := "sXE2MDeW8os6ywv1oUWaFEPFcSPCEb/msQ/NZGKNA9Emm/e42axaAPojzfkZ9Hacyw/iS/5nH4YIkczMgXu3z5fAwFjumxtf3OxbqvUcQ80wvw2/7B5aQmsF6ZwrCFHZ+L/cj9/bg7L1EGUGtdyDzoRKti4zf9QF/03OsP7QljI=" + expectedAuthorizationHeader := fmt.Sprintf( + `Signature keyId="my-key-id",`+ + `algorithm="hs2019",headers="(request-target) host content-type digest",`+ + `signature="%s"`, expectedSignature) + if authorizationHeaderValue != expectedAuthorizationHeader { + t.Errorf("Authorization header value is incorrect. Got\n'%s'\nbut expected\n'%s'", authorizationHeaderValue, expectedAuthorizationHeader) + } + + // Test with PSS signature. The PSS signature creates a new signature every time it is invoked. + authConfig = sw.HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: sw.HttpSigningSchemeHs2019, + SigningAlgorithm: sw.HttpSigningAlgorithmRsaPSS, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + } + authorizationHeaderValue = executeHttpSignatureAuth(t, &authConfig, true) + expectedSignature = `[a-zA-Z0-9+/]+=` + expectedAuthorizationHeader = fmt.Sprintf( + `Signature keyId="my-key-id",`+ + `algorithm="hs2019",headers="\(request-target\) host content-type digest",`+ + `signature="%s"`, expectedSignature) + re := regexp.MustCompile(expectedAuthorizationHeader) + if !re.MatchString(authorizationHeaderValue) { + t.Errorf("Authorization header value is incorrect. Got\n'%s'\nbut expected\n'%s'", authorizationHeaderValue, expectedAuthorizationHeader) + } +} diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go new file mode 100644 index 000000000000..4d5b399f8464 --- /dev/null +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go @@ -0,0 +1,371 @@ +/* + * OpenAPI Petstore + * + * This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\ + * + * API version: 1.0.0 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package openapi + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "io/ioutil" + "net/textproto" + "os" + "time" +) + +const ( + // Constants for HTTP signature parameters. + // The '(request-target)' parameter concatenates the lowercased :method, an + // ASCII space, and the :path pseudo-headers. + HttpSignatureParameterRequestTarget string = "(request-target)" + // The '(created)' parameter expresses when the signature was + // created. The value MUST be a Unix timestamp integer value. + HttpSignatureParameterCreated string = "(created)" + // The '(expires)' parameter expresses when the signature ceases to + // be valid. The value MUST be a Unix timestamp integer value. + HttpSignatureParameterExpires string = "(expires)" +) + +const ( + // Constants for HTTP headers. + // The 'Host' header, as defined in RFC 2616, section 14.23. + HttpHeaderHost string = "Host" + // The 'Date' header. + HttpHeaderDate string = "Date" + // The digest header, as defined in RFC 3230, section 4.3.2. + HttpHeaderDigest string = "Digest" + // The HTTP Authorization header, as defined in RFC 7235, section 4.2. + HttpHeaderAuthorization string = "Authorization" +) + +const ( + // Specifies the Digital Signature Algorithm is derived from metadata + // associated with 'keyId'. Supported DSA algorithms are RSASSA-PKCS1-v1_5, + // RSASSA-PSS, and ECDSA. + // The hash is SHA-512. + // This is the default value. + HttpSigningSchemeHs2019 string = "hs2019" + // Use RSASSA-PKCS1-v1_5 with SHA-512 hash. Deprecated. + HttpSigningSchemeRsaSha512 string = "rsa-sha512" + // Use RSASSA-PKCS1-v1_5 with SHA-256 hash. Deprecated. + HttpSigningSchemeRsaSha256 string = "rsa-sha256" + + // RFC 8017 section 7.2 + // Calculate the message signature using RSASSA-PKCS1-V1_5-SIGN from RSA PKCS#1 v1.5. + // PKCSV1_5 is deterministic. The same message and key will produce an identical + // signature value each time. + HttpSigningAlgorithmRsaPKCS1v15 string = "RSASSA-PKCS1-v1_5" + // Calculate the message signature using probabilistic signature scheme RSASSA-PSS. + // PSS is randomized and will produce a different signature value each time. + HttpSigningAlgorithmRsaPSS string = "RSASSA-PSS" +) + +// HttpSignatureAuth provides HTTP signature authentication to a request passed +// via context using ContextHttpSignatureAuth. +// An 'Authorization' header is calculated by creating a hash of select headers, +// and optionally the body of the HTTP request, then signing the hash value using +// a private key which is available to the client. +// +// SignedHeaders specifies the list of HTTP headers that are included when generating +// the message signature. +// The two special signature headers '(request-target)' and '(created)' SHOULD be +// included in SignedHeaders. +// The '(created)' header expresses when the signature was created. +// The '(request-target)' header is a concatenation of the lowercased :method, an +// ASCII space, and the :path pseudo-headers. +// +// For example, SignedHeaders can be set to: +// (request-target) (created) date host digest +// +// When SignedHeaders is not specified, the client defaults to a single value, '(created)', +// in the list of HTTP headers. +// When SignedHeaders contains the 'Digest' value, the client performs the following operations: +// 1. Calculate a digest of request body, as specified in RFC3230, section 4.3.2. +// 2. Set the 'Digest' header in the request body. +// 3. Include the 'Digest' header and value in the HTTP signature. +type HttpSignatureAuth struct { + KeyId string // A key identifier. + PrivateKeyPath string // The path to the private key. + Passphrase string // The passphrase to decrypt the private key, if the key is encrypted. + SigningScheme string // The signature scheme, when signing HTTP requests. Supported value is 'hs2019'. + // The signature algorithm, when signing HTTP requests. + // Supported values are RSASSA-PKCS1-v1_5, RSASSA-PSS. + SigningAlgorithm string + SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. + // SignatureMaxValidity specifies the maximum duration of the signature validity. + // The value is used to set the '(expires)' signature parameter in the HTTP request. + // '(expires)' is set to '(created)' plus the value of the SignatureMaxValidity field. + // To specify the '(expires)' signature parameter, set 'SignatureMaxValidity' and add '(expires)' to 'SignedHeaders'. + SignatureMaxValidity time.Duration + privateKey crypto.PrivateKey // The private key used to sign HTTP requests. +} + +// ContextWithValue validates the HttpSignatureAuth configuration parameters and returns a context +// suitable for HTTP signature. An error is returned if the HttpSignatureAuth configuration parameters +// are invalid. +func (h *HttpSignatureAuth) ContextWithValue(ctx context.Context) (context.Context, error) { + if err := h.loadPrivateKey(); err != nil { + return err + } + return context.WithValue(ctx, ContextHttpSignatureAuth, *h), nil +} + +// loadPrivateKey reads the private key from the file specified in the HttpSignatureAuth. +func (h *HttpSignatureAuth) loadPrivateKey() (err error) { + var file *os.File + file, err = os.Open(h.PrivateKeyPath) + if err != nil { + return err + } + defer func() { + err = file.Close() + }() + var priv []byte + priv, err = ioutil.ReadAll(file) + if err != nil { + return err + } + if !strings.HasSuffix(h.PrivateKeyPath, ".pem") { + return fmt.Errorf("Private key must be in PEM format.") + } + pemBlock, _ := pem.Decode(priv) + if pemBlock == nil { + // No PEM data has been found. + return fmt.Errorf("File '%s' does not contain PEM data", h.PrivateKeyPath) + } + var privKey []byte + if x509.IsEncryptedPEMBlock(pemBlock) { + // The PEM data is encrypted. + privKey, err = x509.DecryptPEMBlock(pemBlock, h.Passphrase) + if err != nil { + // Failed to decrypt PEM block. Because of deficiencies in the encrypted-PEM format, + // it's not always possibleto detect an incorrect password. + return err + } + } else { + privKey = pemBlock.Bytes + } + switch pemBlock.Type { + case "RSA PRIVATE KEY": + if h.privateKey, err = x509.ParsePKCS1PrivateKey(privKey); err != nil { + return err + } + case "EC PRIVATE KEY", "PRIVATE KEY": + // https://tools.ietf.org/html/rfc5915 section 4. + if h.privateKey, err = x509.ParsePKCS8PrivateKey(privKey); err != nil { + return err + } + default: + return fmt.Errorf("Key '%s' is not supported", pemBlock.Type) + } + return nil +} + +// SignRequest signs the request using HTTP signature. +// See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ +// +// Do not add, remove or change headers that are included in the SignedHeaders +// after SignRequest has been invoked; this is because the header values are +// included in the signature. Any subsequent alteration will cause a signature +// verification failure. +// If there are multiple instances of the same header field, all +// header field values associated with the header field MUST be +// concatenated, separated by a ASCII comma and an ASCII space +// ', ', and used in the order in which they will appear in the +// transmitted HTTP message. +func SignRequest( + ctx context.Context, + r *http.Request, + auth HttpSignatureAuth) error { + + if auth.privateKey == nil { + return errors.New("Private key is not set") + } + now := time.Now() + date := now.UTC().Format(http.TimeFormat) + // The 'created' field expresses when the signature was created. + // The value MUST be a Unix timestamp integer value. See 'HTTP signature' section 2.1.4. + created := now.Unix() + + var h crypto.Hash + var err error + var prefix string + var expiresUnix float64 + + if auth.SignatureMaxValidity < 0 { + return fmt.Errorf("Signature validity must be a positive value") + } + if auth.SignatureMaxValidity > 0 { + e := now.Add(auth.SignatureMaxValidity) + expiresUnix = float64(e.Unix()) + float64(e.Nanosecond()) / float64(time.Second) + } + // Determine the cryptographic hash to be used for the signature and the body digest. + switch auth.SigningScheme { + case HttpSigningSchemeRsaSha512, HttpSigningSchemeHs2019: + h = crypto.SHA512 + prefix = "SHA-512=" + case HttpSigningSchemeRsaSha256: + // This is deprecated and should no longer be used. + h = crypto.SHA256 + prefix = "SHA-256=" + default: + return fmt.Errorf("Unsupported signature scheme: %v", auth.SigningScheme) + } + if !h.Available() { + return fmt.Errorf("Hash '%v' is not available", h) + } + + // Build the "(request-target)" signature header. + var sb bytes.Buffer + fmt.Fprintf(&sb, "%s %s", strings.ToLower(r.Method), r.URL.EscapedPath()) + if r.URL.RawQuery != "" { + // The ":path" pseudo-header field includes the path and query parts + // of the target URI (the "path-absolute" production and optionally a + // '?' character followed by the "query" production (see Sections 3.3 + // and 3.4 of [RFC3986] + fmt.Fprintf(&sb, "?%s", r.URL.RawQuery) + } + requestTarget := sb.String() + sb.Reset() + + // Build the string to be signed. + signedHeaders := auth.SignedHeaders + if len(signedHeaders) == 0 { + signedHeaders = []string{HttpSignatureParameterCreated} + } + // Validate the list of signed headers has no duplicates. + m := make(map[string]bool) + for _, h := range signedHeaders { + m[h] = true + } + if len(m) != len(signedHeaders) { + return fmt.Errorf("List of signed headers must not have any duplicates") + } + hasCreatedParameter := false + hasExpiresParameter := false + for i, header := range signedHeaders { + header = strings.ToLower(header) + var value string + switch header { + case strings.ToLower(HttpHeaderAuthorization): + return fmt.Errorf("Cannot include the 'Authorization' header as a signed header.") + case HttpSignatureParameterRequestTarget: + value = requestTarget + case HttpSignatureParameterCreated: + value = fmt.Sprintf("%d", created) + hasCreatedParameter = true + case HttpSignatureParameterExpires: + if auth.SignatureMaxValidity.Nanoseconds() == 0 { + return fmt.Errorf("Cannot set '(expires)' signature parameter. SignatureMaxValidity is not configured.") + } + value = fmt.Sprintf("%.3f", expiresUnix) + hasExpiresParameter = true + case "date": + value = date + r.Header.Set(HttpHeaderDate, date) + case "digest": + // Calculate the digest of the HTTP request body. + // Calculate body digest per RFC 3230 section 4.3.2 + bodyHash := h.New() + if r.Body != nil { + // Make a copy of the body io.Reader so that we can read the body to calculate the hash, + // then one more time when marshaling the request. + var body io.Reader + body, err = r.GetBody() + if err != nil { + return err + } + if _, err = io.Copy(bodyHash, body); err != nil { + return err + } + } + d := bodyHash.Sum(nil) + value = prefix + base64.StdEncoding.EncodeToString(d) + r.Header.Set(HttpHeaderDigest, value) + case "host": + value = r.Host + r.Header.Set(HttpHeaderHost, r.Host) + default: + var ok bool + var v []string + canonicalHeader := textproto.CanonicalMIMEHeaderKey(header) + if v, ok = r.Header[canonicalHeader]; !ok { + // If a header specified in the headers parameter cannot be matched with + // a provided header in the message, the implementation MUST produce an error. + return fmt.Errorf("Header '%s' does not exist in the request", canonicalHeader) + } + // If there are multiple instances of the same header field, all + // header field values associated with the header field MUST be + // concatenated, separated by a ASCII comma and an ASCII space + // `, `, and used in the order in which they will appear in the + // transmitted HTTP message. + value = strings.Join(v, ", ") + } + if i > 0 { + fmt.Fprintf(&sb, "\n") + } + fmt.Fprintf(&sb, "%s: %s", header, value) + } + if expiresUnix != 0 && !hasExpiresParameter { + return fmt.Errorf("SignatureMaxValidity is specified, but '(expired)' parameter is not present") + } + msg := []byte(sb.String()) + msgHash := h.New() + if _, err = msgHash.Write(msg); err != nil { + return err + } + d := msgHash.Sum(nil) + + var signature []byte + switch key := auth.privateKey.(type) { + case *rsa.PrivateKey: + switch auth.SigningAlgorithm { + case HttpSigningAlgorithmRsaPKCS1v15: + signature, err = rsa.SignPKCS1v15(rand.Reader, key, h, d) + case "", HttpSigningAlgorithmRsaPSS: + signature, err = rsa.SignPSS(rand.Reader, key, h, d, nil) + default: + return fmt.Errorf("Unsupported signing algorithm: '%s'", auth.SigningAlgorithm) + } + case *ecdsa.PrivateKey: + signature, err = key.Sign(rand.Reader, d, h) + //case ed25519.PrivateKey: requires go 1.13 + // signature, err = key.Sign(rand.Reader, msg, crypto.Hash(0)) + default: + return fmt.Errorf("Unsupported private key") + } + if err != nil { + return err + } + + sb.Reset() + for i, header := range signedHeaders { + if i > 0 { + sb.WriteRune(' ') + } + sb.WriteString(strings.ToLower(header)) + } + headers_list := sb.String() + sb.Reset() + fmt.Fprintf(&sb, `Signature keyId="%s",algorithm="%s",`, auth.KeyId, auth.SigningScheme) + if hasCreatedParameter { + fmt.Fprintf(&sb, "created=%d,", created) + } + if hasExpiresParameter { + fmt.Fprintf(&sb, "expires=%.3f,", expiresUnix) + } + fmt.Fprintf(&sb, `headers="%s",signature="%s"`, headers_list, base64.StdEncoding.EncodeToString(signature)) + authStr := sb.String() + r.Header.Set(HttpHeaderAuthorization, authStr) + return nil +} From 7b3a54b277601c0c43474b99449ec8046704b800 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Tue, 21 Jan 2020 14:53:39 -0800 Subject: [PATCH 43/48] change URL of apache license to use https --- ...h-fake-endpoints-models-for-testing-with-http-signature.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml b/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml index cf45bf76e379..59d96166c7e2 100644 --- a/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml @@ -8,7 +8,7 @@ info: title: OpenAPI Petstore license: name: Apache-2.0 - url: 'http://www.apache.org/licenses/LICENSE-2.0.html' + url: 'https://www.apache.org/licenses/LICENSE-2.0.html' tags: - name: pet description: Everything about your Pets From f1f49732a390547a76463c8c1846738819c8a957 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Wed, 22 Jan 2020 19:23:36 -0800 Subject: [PATCH 44/48] http signature unit tests --- .../http_signature_test.mustache | 242 +++++++++++------- .../go-experimental/signing.mustache | 14 +- .../go-petstore/http_signature_test.go | 242 +++++++++++------- .../go-experimental/go-petstore/signing.go | 14 +- 4 files changed, 312 insertions(+), 200 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache b/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache index 13fbf3d53de2..02c7a0cb3753 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache @@ -12,6 +12,7 @@ import ( "io/ioutil" "net/http/httputil" "os" + "path/filepath" "regexp" "strings" "testing" @@ -36,8 +37,8 @@ G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI 7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== -----END RSA PRIVATE KEY-----` -func writeTestRsaPemKey(t *testing.T, fileName string) { - err := ioutil.WriteFile(fileName, []byte(rsaTestPrivateKey), 0644) +func writeTestRsaPemKey(t *testing.T, filePath string) { + err := ioutil.WriteFile(filePath, []byte(rsaTestPrivateKey), 0644) if err != nil { t.Fatalf("Error writing private key: %v", err) } @@ -51,13 +52,13 @@ const ( keyFormatPkcs8Der // Private key is serialized as PKCS#8 encoded in DER format. ) -func writeRandomTestRsaPemKey(t *testing.T, fileName string, bits int, format keyFormat, passphrase string, alg *x509.PEMCipher) { +func writeRandomTestRsaPemKey(t *testing.T, filePath string, bits int, format keyFormat, passphrase string, alg *x509.PEMCipher) { key, err := rsa.GenerateKey(rand.Reader, bits) if err != nil { t.Fatalf("Error generating RSA private key file: %v", err) } var outFile *os.File - outFile, err = os.Create(fileName) + outFile, err = os.Create(filePath) if err != nil { t.Fatalf("Error creating RSA private key file: %v", err) } @@ -66,52 +67,62 @@ func writeRandomTestRsaPemKey(t *testing.T, fileName string, bits int, format ke switch format { case keyFormatPem: if passphrase != "" { - f.Fatalf("Encrypting PKCS#1-encoded private key with passphrase is not supported") + t.Fatalf("Encrypting PKCS#1-encoded private key with passphrase is not supported") } privKeyBytes = x509.MarshalPKCS1PrivateKey(key) case keyFormatPkcs8Pem: - privKeyBytes = x509.MarshalPKCS8PrivateKey(key) + privKeyBytes, err = x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatalf("Error writing private key: %v", err) + } case keyFormatPkcs8Der: if passphrase != "" { - f.Fatalf("Encrypting DER-encoded private key with passphrase is not supported") + t.Fatalf("Encrypting DER-encoded private key with passphrase is not supported") + } + privKeyBytes, err = x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatalf("Error writing private key: %v", err) } - _, err = outFile.Write(x509.MarshalPKCS8PrivateKey(key)) + _, err = outFile.Write(privKeyBytes) if err != nil { - t.Fatalf("Error writing RSA private key: %v", err) + t.Fatalf("Error writing DER-encoded private key: %v", err) } - return default: t.Fatalf("Unsupported key format: %v", format) } - var pemBlock *pem.Block - if passphrase == "" { - pemBlock = &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privKeyBytes, + switch format { + case keyFormatPem, keyFormatPkcs8Der: + var pemBlock *pem.Block + if passphrase == "" { + pemBlock = &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privKeyBytes, + } + } else { + pemBlock, err = x509.EncryptPEMBlock(rand.Reader, "ENCRYPTED PRIVATE KEY", privKeyBytes, []byte(passphrase), *alg) + if err != nil { + t.Fatalf("Error encoding RSA private key: %v", err) + } } - } else { - pemBlock, err = x509.EncryptPEMBlock(rand.Reader, "ENCRYPTED PRIVATE KEY", privKeyBytes, passphrase, *alg) + err = pem.Encode(outFile, pemBlock) if err != nil { t.Fatalf("Error encoding RSA private key: %v", err) } } - err = pem.Encode(outFile, pemBlock) - if err != nil { - t.Fatalf("Error encoding RSA private key: %v", err) - } + fmt.Printf("Wrote private key '%s'\n", filePath) } /* Commented out because OpenAPITools is configured to use golang 1.8 at build time x509.MarshalPKCS8PrivateKey is not present. -func writeRandomTestEcdsaPemKey(t *testing.T, fileName string) { +func writeRandomTestEcdsaPemKey(t *testing.T, filePath string) { key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) if err != nil { t.Fatalf("Error generating ECDSA private key file: %v", err) } var outFile *os.File - outFile, err = os.Create(fileName) + outFile, err = os.Create(filePath) if err != nil { t.Fatalf("Error creating ECDSA private key file: %v", err) } @@ -138,6 +149,14 @@ func writeRandomTestEcdsaPemKey(t *testing.T, fileName string) { // clear-text and password encrypted. // Test unmarshaling of the private key. func TestHttpSignaturePrivateKeys(t *testing.T) { + var err error + var dir string + dir, err = ioutil.TempDir("", "go-http-sign") + if err != nil { + t.Fatalf("Failed to create temporary directory") + } + defer os.RemoveAll(dir) + pemCiphers := []x509.PEMCipher{ x509.PEMCipherDES, x509.PEMCipher3DES, @@ -148,13 +167,6 @@ func TestHttpSignaturePrivateKeys(t *testing.T) { // Test RSA private keys with various key lengths. for _, bits := range []int{1024, 2048, 3072, 4096} { - // Generate keys in PEM format. - authConfig := sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: "hs2019", - SignedHeaders: []string{"Content-Type"}, - } - for _, format := range []keyFormat{keyFormatPem, keyFormatPkcs8Pem, keyFormatPkcs8Der} { // Generate test private key. var privateKeyPath string @@ -163,18 +175,44 @@ func TestHttpSignaturePrivateKeys(t *testing.T) { privateKeyPath = "privatekey.pem" case keyFormatPkcs8Der: privateKeyPath = "privatekey.der" + default: + t.Fatalf("Unsupported private key format: %v", format) } + privateKeyPath = filepath.Join(dir, privateKeyPath) + // Generate keys in PEM format. writeRandomTestRsaPemKey(t, privateKeyPath, bits, format, "", nil) - err := authConfig.LoadPrivateKey(privateKeyPath) + + authConfig := HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: privateKeyPath, + Passphrase: "", + SigningScheme: "hs2019", + SignedHeaders: []string{"Content-Type"}, + } + + // Create a context with the HTTP signature configuration parameters. + _, err = authConfig.ContextWithValue(context.Background()) if err != nil { - t.Fatalf("Error loading private key: %v", err) + t.Fatalf("Error loading private key '%s': %v", privateKeyPath, err) } - for _, alg := range pemCiphers { - writeRandomTestRsaPemKey(t, privateKeyPath, bits, format, passphrase, alg) - err := authConfig.LoadPrivateKey(privateKeyPath) - if err != nil { - t.Fatalf("Error loading private key: %v", err) + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: privateKeyPath, + Passphrase: "my-secret-passphrase", + SigningScheme: "hs2019", + SignedHeaders: []string{"Content-Type"}, + } + switch format { + case keyFormatPem: + // Do nothing. Keys cannot be encrypted when using PKCS#1. + case keyFormatPkcs8Pem: + for _, alg := range pemCiphers { + writeRandomTestRsaPemKey(t, privateKeyPath, bits, format, authConfig.Passphrase, &alg) + _, err := authConfig.ContextWithValue(context.Background()) + if err != nil { + t.Fatalf("Error loading private key '%s': %v", privateKeyPath, err) + } } } } @@ -185,7 +223,7 @@ func TestHttpSignaturePrivateKeys(t *testing.T) { of golang that does not support ECDSA keys. { privateKeyPath := "privatekey.pem" - authConfig := sw.HttpSignatureAuth{ + authConfig := HttpSignatureAuth{ KeyId: "my-key-id", SigningScheme: "hs2019", SignedHeaders: []string{"Content-Type"}, @@ -194,36 +232,48 @@ func TestHttpSignaturePrivateKeys(t *testing.T) { writeRandomTestEcdsaPemKey(t, privateKeyPath) err := authConfig.LoadPrivateKey(privateKeyPath) if err != nil { - t.Fatalf("Error loading private key: %v", err) + t.Fatalf("Error loading private key '%s': %v", privateKeyPath, err) } } */ } -func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, expectSuccess bool) string { - cfg := sw.NewConfiguration() +const testHost = "petstore.swagger.io:80" +const testScheme = "http" + +func executeHttpSignatureAuth(t *testing.T, authConfig *HttpSignatureAuth, expectSuccess bool) string { + var err error + var dir string + dir, err = ioutil.TempDir("", "go-http-sign") + if err != nil { + t.Fatalf("Failed to create temporary directory") + } + defer os.RemoveAll(dir) + + cfg := NewConfiguration() cfg.AddDefaultHeader("testheader", "testvalue") cfg.AddDefaultHeader("Content-Type", "application/json") cfg.Host = testHost cfg.Scheme = testScheme - apiClient := sw.NewAPIClient(cfg) + apiClient := NewAPIClient(cfg) - privateKeyPath := "rsa.pem" + privateKeyPath := filepath.Join(dir, "rsa.pem") writeTestRsaPemKey(t, privateKeyPath) - err := authConfig.LoadPrivateKey(privateKeyPath) + authConfig.PrivateKeyPath = privateKeyPath + var authCtx context.Context + authCtx, err = authConfig.ContextWithValue(context.Background()) if err != nil { - t.Fatalf("Error loading private key: %v", err) + t.Fatalf("Error validating HTTP signature configuration: %v", err) } - authCtx := context.WithValue(context.Background(), sw.ContextHttpSignatureAuth, *authConfig) - newPet := (sw.Pet{Id: sw.PtrInt64(12992), Name: "gopher", + newPet := (Pet{Id: PtrInt64(12992), Name: "gopher", PhotoUrls: []string{"http://1.com", "http://2.com"}, - Status: sw.PtrString("pending"), - Tags: &[]sw.Tag{sw.Tag{Id: sw.PtrInt64(1), Name: sw.PtrString("tag2")}}}) + Status: PtrString("pending"), + Tags: &[]Tag{Tag{Id: PtrInt64(1), Name: PtrString("tag2")}}}) fmt.Printf("Request with HTTP signature. Scheme: '%s'. Algorithm: '%s'. MaxValidity: %v. Headers: '%v'\n", authConfig.SigningScheme, authConfig.SigningAlgorithm, authConfig.SignatureMaxValidity, authConfig.SignedHeaders) - r, err2 := apiClient.PetApi.AddPet(authCtx).Body(newPet).Execute() + r, err2 := apiClient.PetApi.AddPet(authCtx).Pet(newPet).Execute() if expectSuccess && err2 != nil { t.Fatalf("Error while adding pet: %v", err2) } @@ -265,10 +315,10 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, ex } else { for _, header := range authConfig.SignedHeaders { header = strings.ToLower(header) - if header == sw.HttpSignatureParameterCreated { + if header == HttpSignatureParameterCreated { fmt.Fprintf(&sb, `created=[0-9]+,`) } - if header == sw.HttpSignatureParameterExpires { + if header == HttpSignatureParameterExpires { fmt.Fprintf(&sb, `expires=[0-9]+\.[0-9]{3},`) } } @@ -288,7 +338,7 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, ex case "digest": var prefix string switch authConfig.SigningScheme { - case sw.HttpSigningSchemeRsaSha256: + case HttpSigningSchemeRsaSha256: prefix = "SHA-256=" default: prefix = "SHA-512=" @@ -299,7 +349,7 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, ex } } if len(authConfig.SignedHeaders) == 0 { - fmt.Fprintf(&sb, regexp.QuoteMeta(sw.HttpSignatureParameterCreated)) + fmt.Fprintf(&sb, regexp.QuoteMeta(HttpSignatureParameterCreated)) } fmt.Fprintf(&sb, `",signature="[a-zA-Z0-9+/]+="`) re := regexp.MustCompile(sb.String()) @@ -312,12 +362,12 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, ex func TestHttpSignatureAuth(t *testing.T) { // Test with 'hs2019' signature scheme, and default signature algorithm (RSA SSA PKCS1.5) - authConfig := sw.HttpSignatureAuth{ + authConfig := HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. - sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. "Date", // The date and time at which the message was originated. "Content-Type", // The Media type of the body of the request. @@ -327,85 +377,85 @@ func TestHttpSignatureAuth(t *testing.T) { executeHttpSignatureAuth(t, &authConfig, true) // Test with duplicate headers. This is invalid and should be rejected. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignedHeaders: []string{"Host", "Date", "Host"}, } executeHttpSignatureAuth(t, &authConfig, false) // Test with non-existent header. This is invalid and should be rejected. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignedHeaders: []string{"Host", "Date", "Garbage-HeaderDoesNotExist"}, } executeHttpSignatureAuth(t, &authConfig, false) // Test with 'Authorization' header in the signed headers. This is invalid and should be rejected. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignedHeaders: []string{"Host", "Authorization"}, } executeHttpSignatureAuth(t, &authConfig, false) // Specify signature max validity, but '(expires)' parameter is missing. This should cause an error. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignatureMaxValidity: 7 * time.Minute, } executeHttpSignatureAuth(t, &authConfig, false) // Specify invalid signature max validity. This should cause an error. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignatureMaxValidity: -3 * time.Minute, } executeHttpSignatureAuth(t, &authConfig, false) // Specify signature max validity and '(expires)' parameter. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignatureMaxValidity: time.Minute, SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. - sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. - sw.HttpSignatureParameterExpires, // Time when signature expires. + HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + HttpSignatureParameterExpires, // Time when signature expires. }, } executeHttpSignatureAuth(t, &authConfig, true) // Specify '(expires)' parameter but no signature max validity. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. - sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. - sw.HttpSignatureParameterExpires, // Time when signature expires. + HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + HttpSignatureParameterExpires, // Time when signature expires. }, } executeHttpSignatureAuth(t, &authConfig, false) // Test with empty signed headers. The client should automatically add the "(created)" parameter by default. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignedHeaders: []string{}, } executeHttpSignatureAuth(t, &authConfig, true) // Test with deprecated RSA-SHA512, some servers may still support it. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeRsaSha512, + SigningScheme: HttpSigningSchemeRsaSha512, SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. - sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. "Date", // The date and time at which the message was originated. "Content-Type", // The Media type of the body of the request. @@ -415,12 +465,12 @@ func TestHttpSignatureAuth(t *testing.T) { executeHttpSignatureAuth(t, &authConfig, true) // Test with deprecated RSA-SHA256, some servers may still support it. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeRsaSha256, + SigningScheme: HttpSigningSchemeRsaSha256, SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. - sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. "Date", // The date and time at which the message was originated. "Content-Type", // The Media type of the body of the request. @@ -431,12 +481,12 @@ func TestHttpSignatureAuth(t *testing.T) { // Test with headers without date. This makes it possible to get a fixed signature, used for unit test purpose. // This should not be used in production code as it could potentially allow replay attacks. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, - SigningAlgorithm: sw.HttpSigningAlgorithmRsaPKCS1v15, + SigningScheme: HttpSigningSchemeHs2019, + SigningAlgorithm: HttpSigningAlgorithmRsaPKCS1v15, SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, + HttpSignatureParameterRequestTarget, "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. "Content-Type", // The Media type of the body of the request. "Digest", // A cryptographic digest of the request body. @@ -453,12 +503,12 @@ func TestHttpSignatureAuth(t *testing.T) { } // Test with PSS signature. The PSS signature creates a new signature every time it is invoked. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, - SigningAlgorithm: sw.HttpSigningAlgorithmRsaPSS, + SigningScheme: HttpSigningSchemeHs2019, + SigningAlgorithm: HttpSigningAlgorithmRsaPSS, SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, + HttpSignatureParameterRequestTarget, "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. "Content-Type", // The Media type of the body of the request. "Digest", // A cryptographic digest of the request body. diff --git a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache index d95359aa8fe0..aa5259201571 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache @@ -2,6 +2,8 @@ package {{packageName}} import ( + "bytes" + "context" "crypto" "crypto/ecdsa" "crypto/rand" @@ -9,9 +11,13 @@ import ( "crypto/x509" "encoding/base64" "encoding/pem" + "fmt" + "io" "io/ioutil" + "net/http" "net/textproto" "os" + "strings" "time" ) @@ -107,7 +113,7 @@ type HttpSignatureAuth struct { // are invalid. func (h *HttpSignatureAuth) ContextWithValue(ctx context.Context) (context.Context, error) { if err := h.loadPrivateKey(); err != nil { - return err + return nil, err } return context.WithValue(ctx, ContextHttpSignatureAuth, *h), nil } @@ -117,7 +123,7 @@ func (h *HttpSignatureAuth) loadPrivateKey() (err error) { var file *os.File file, err = os.Open(h.PrivateKeyPath) if err != nil { - return err + return fmt.Errorf("Cannot load private key '%s'. Error: %v", h.PrivateKeyPath, err) } defer func() { err = file.Close() @@ -138,7 +144,7 @@ func (h *HttpSignatureAuth) loadPrivateKey() (err error) { var privKey []byte if x509.IsEncryptedPEMBlock(pemBlock) { // The PEM data is encrypted. - privKey, err = x509.DecryptPEMBlock(pemBlock, h.Passphrase) + privKey, err = x509.DecryptPEMBlock(pemBlock, []byte(h.Passphrase)) if err != nil { // Failed to decrypt PEM block. Because of deficiencies in the encrypted-PEM format, // it's not always possibleto detect an incorrect password. @@ -181,7 +187,7 @@ func SignRequest( auth HttpSignatureAuth) error { if auth.privateKey == nil { - return errors.New("Private key is not set") + return fmt.Errorf("Private key is not set") } now := time.Now() date := now.UTC().Format(http.TimeFormat) diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go index 2623c0ab4902..6c92fa979c29 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go @@ -20,6 +20,7 @@ import ( "io/ioutil" "net/http/httputil" "os" + "path/filepath" "regexp" "strings" "testing" @@ -44,8 +45,8 @@ G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI 7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== -----END RSA PRIVATE KEY-----` -func writeTestRsaPemKey(t *testing.T, fileName string) { - err := ioutil.WriteFile(fileName, []byte(rsaTestPrivateKey), 0644) +func writeTestRsaPemKey(t *testing.T, filePath string) { + err := ioutil.WriteFile(filePath, []byte(rsaTestPrivateKey), 0644) if err != nil { t.Fatalf("Error writing private key: %v", err) } @@ -59,13 +60,13 @@ const ( keyFormatPkcs8Der // Private key is serialized as PKCS#8 encoded in DER format. ) -func writeRandomTestRsaPemKey(t *testing.T, fileName string, bits int, format keyFormat, passphrase string, alg *x509.PEMCipher) { +func writeRandomTestRsaPemKey(t *testing.T, filePath string, bits int, format keyFormat, passphrase string, alg *x509.PEMCipher) { key, err := rsa.GenerateKey(rand.Reader, bits) if err != nil { t.Fatalf("Error generating RSA private key file: %v", err) } var outFile *os.File - outFile, err = os.Create(fileName) + outFile, err = os.Create(filePath) if err != nil { t.Fatalf("Error creating RSA private key file: %v", err) } @@ -74,52 +75,62 @@ func writeRandomTestRsaPemKey(t *testing.T, fileName string, bits int, format ke switch format { case keyFormatPem: if passphrase != "" { - f.Fatalf("Encrypting PKCS#1-encoded private key with passphrase is not supported") + t.Fatalf("Encrypting PKCS#1-encoded private key with passphrase is not supported") } privKeyBytes = x509.MarshalPKCS1PrivateKey(key) case keyFormatPkcs8Pem: - privKeyBytes = x509.MarshalPKCS8PrivateKey(key) + privKeyBytes, err = x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatalf("Error writing private key: %v", err) + } case keyFormatPkcs8Der: if passphrase != "" { - f.Fatalf("Encrypting DER-encoded private key with passphrase is not supported") + t.Fatalf("Encrypting DER-encoded private key with passphrase is not supported") + } + privKeyBytes, err = x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatalf("Error writing private key: %v", err) } - _, err = outFile.Write(x509.MarshalPKCS8PrivateKey(key)) + _, err = outFile.Write(privKeyBytes) if err != nil { - t.Fatalf("Error writing RSA private key: %v", err) + t.Fatalf("Error writing DER-encoded private key: %v", err) } - return default: t.Fatalf("Unsupported key format: %v", format) } - var pemBlock *pem.Block - if passphrase == "" { - pemBlock = &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privKeyBytes, + switch format { + case keyFormatPem, keyFormatPkcs8Der: + var pemBlock *pem.Block + if passphrase == "" { + pemBlock = &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privKeyBytes, + } + } else { + pemBlock, err = x509.EncryptPEMBlock(rand.Reader, "ENCRYPTED PRIVATE KEY", privKeyBytes, []byte(passphrase), *alg) + if err != nil { + t.Fatalf("Error encoding RSA private key: %v", err) + } } - } else { - pemBlock, err = x509.EncryptPEMBlock(rand.Reader, "ENCRYPTED PRIVATE KEY", privKeyBytes, passphrase, *alg) + err = pem.Encode(outFile, pemBlock) if err != nil { t.Fatalf("Error encoding RSA private key: %v", err) } } - err = pem.Encode(outFile, pemBlock) - if err != nil { - t.Fatalf("Error encoding RSA private key: %v", err) - } + fmt.Printf("Wrote private key '%s'\n", filePath) } /* Commented out because OpenAPITools is configured to use golang 1.8 at build time x509.MarshalPKCS8PrivateKey is not present. -func writeRandomTestEcdsaPemKey(t *testing.T, fileName string) { +func writeRandomTestEcdsaPemKey(t *testing.T, filePath string) { key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) if err != nil { t.Fatalf("Error generating ECDSA private key file: %v", err) } var outFile *os.File - outFile, err = os.Create(fileName) + outFile, err = os.Create(filePath) if err != nil { t.Fatalf("Error creating ECDSA private key file: %v", err) } @@ -146,6 +157,14 @@ func writeRandomTestEcdsaPemKey(t *testing.T, fileName string) { // clear-text and password encrypted. // Test unmarshaling of the private key. func TestHttpSignaturePrivateKeys(t *testing.T) { + var err error + var dir string + dir, err = ioutil.TempDir("", "go-http-sign") + if err != nil { + t.Fatalf("Failed to create temporary directory") + } + defer os.RemoveAll(dir) + pemCiphers := []x509.PEMCipher{ x509.PEMCipherDES, x509.PEMCipher3DES, @@ -156,13 +175,6 @@ func TestHttpSignaturePrivateKeys(t *testing.T) { // Test RSA private keys with various key lengths. for _, bits := range []int{1024, 2048, 3072, 4096} { - // Generate keys in PEM format. - authConfig := sw.HttpSignatureAuth{ - KeyId: "my-key-id", - SigningScheme: "hs2019", - SignedHeaders: []string{"Content-Type"}, - } - for _, format := range []keyFormat{keyFormatPem, keyFormatPkcs8Pem, keyFormatPkcs8Der} { // Generate test private key. var privateKeyPath string @@ -171,18 +183,44 @@ func TestHttpSignaturePrivateKeys(t *testing.T) { privateKeyPath = "privatekey.pem" case keyFormatPkcs8Der: privateKeyPath = "privatekey.der" + default: + t.Fatalf("Unsupported private key format: %v", format) } + privateKeyPath = filepath.Join(dir, privateKeyPath) + // Generate keys in PEM format. writeRandomTestRsaPemKey(t, privateKeyPath, bits, format, "", nil) - err := authConfig.LoadPrivateKey(privateKeyPath) + + authConfig := HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: privateKeyPath, + Passphrase: "", + SigningScheme: "hs2019", + SignedHeaders: []string{"Content-Type"}, + } + + // Create a context with the HTTP signature configuration parameters. + _, err = authConfig.ContextWithValue(context.Background()) if err != nil { - t.Fatalf("Error loading private key: %v", err) + t.Fatalf("Error loading private key '%s': %v", privateKeyPath, err) } - for _, alg := range pemCiphers { - writeRandomTestRsaPemKey(t, privateKeyPath, bits, format, passphrase, alg) - err := authConfig.LoadPrivateKey(privateKeyPath) - if err != nil { - t.Fatalf("Error loading private key: %v", err) + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: privateKeyPath, + Passphrase: "my-secret-passphrase", + SigningScheme: "hs2019", + SignedHeaders: []string{"Content-Type"}, + } + switch format { + case keyFormatPem: + // Do nothing. Keys cannot be encrypted when using PKCS#1. + case keyFormatPkcs8Pem: + for _, alg := range pemCiphers { + writeRandomTestRsaPemKey(t, privateKeyPath, bits, format, authConfig.Passphrase, &alg) + _, err := authConfig.ContextWithValue(context.Background()) + if err != nil { + t.Fatalf("Error loading private key '%s': %v", privateKeyPath, err) + } } } } @@ -193,7 +231,7 @@ func TestHttpSignaturePrivateKeys(t *testing.T) { of golang that does not support ECDSA keys. { privateKeyPath := "privatekey.pem" - authConfig := sw.HttpSignatureAuth{ + authConfig := HttpSignatureAuth{ KeyId: "my-key-id", SigningScheme: "hs2019", SignedHeaders: []string{"Content-Type"}, @@ -202,36 +240,48 @@ func TestHttpSignaturePrivateKeys(t *testing.T) { writeRandomTestEcdsaPemKey(t, privateKeyPath) err := authConfig.LoadPrivateKey(privateKeyPath) if err != nil { - t.Fatalf("Error loading private key: %v", err) + t.Fatalf("Error loading private key '%s': %v", privateKeyPath, err) } } */ } -func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, expectSuccess bool) string { - cfg := sw.NewConfiguration() +const testHost = "petstore.swagger.io:80" +const testScheme = "http" + +func executeHttpSignatureAuth(t *testing.T, authConfig *HttpSignatureAuth, expectSuccess bool) string { + var err error + var dir string + dir, err = ioutil.TempDir("", "go-http-sign") + if err != nil { + t.Fatalf("Failed to create temporary directory") + } + defer os.RemoveAll(dir) + + cfg := NewConfiguration() cfg.AddDefaultHeader("testheader", "testvalue") cfg.AddDefaultHeader("Content-Type", "application/json") cfg.Host = testHost cfg.Scheme = testScheme - apiClient := sw.NewAPIClient(cfg) + apiClient := NewAPIClient(cfg) - privateKeyPath := "rsa.pem" + privateKeyPath := filepath.Join(dir, "rsa.pem") writeTestRsaPemKey(t, privateKeyPath) - err := authConfig.LoadPrivateKey(privateKeyPath) + authConfig.PrivateKeyPath = privateKeyPath + var authCtx context.Context + authCtx, err = authConfig.ContextWithValue(context.Background()) if err != nil { - t.Fatalf("Error loading private key: %v", err) + t.Fatalf("Error validating HTTP signature configuration: %v", err) } - authCtx := context.WithValue(context.Background(), sw.ContextHttpSignatureAuth, *authConfig) - newPet := (sw.Pet{Id: sw.PtrInt64(12992), Name: "gopher", + newPet := (Pet{Id: PtrInt64(12992), Name: "gopher", PhotoUrls: []string{"http://1.com", "http://2.com"}, - Status: sw.PtrString("pending"), - Tags: &[]sw.Tag{sw.Tag{Id: sw.PtrInt64(1), Name: sw.PtrString("tag2")}}}) + Status: PtrString("pending"), + Tags: &[]Tag{Tag{Id: PtrInt64(1), Name: PtrString("tag2")}}}) fmt.Printf("Request with HTTP signature. Scheme: '%s'. Algorithm: '%s'. MaxValidity: %v. Headers: '%v'\n", authConfig.SigningScheme, authConfig.SigningAlgorithm, authConfig.SignatureMaxValidity, authConfig.SignedHeaders) - r, err2 := apiClient.PetApi.AddPet(authCtx).Body(newPet).Execute() + r, err2 := apiClient.PetApi.AddPet(authCtx).Pet(newPet).Execute() if expectSuccess && err2 != nil { t.Fatalf("Error while adding pet: %v", err2) } @@ -273,10 +323,10 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, ex } else { for _, header := range authConfig.SignedHeaders { header = strings.ToLower(header) - if header == sw.HttpSignatureParameterCreated { + if header == HttpSignatureParameterCreated { fmt.Fprintf(&sb, `created=[0-9]+,`) } - if header == sw.HttpSignatureParameterExpires { + if header == HttpSignatureParameterExpires { fmt.Fprintf(&sb, `expires=[0-9]+\.[0-9]{3},`) } } @@ -296,7 +346,7 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, ex case "digest": var prefix string switch authConfig.SigningScheme { - case sw.HttpSigningSchemeRsaSha256: + case HttpSigningSchemeRsaSha256: prefix = "SHA-256=" default: prefix = "SHA-512=" @@ -307,7 +357,7 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, ex } } if len(authConfig.SignedHeaders) == 0 { - fmt.Fprintf(&sb, regexp.QuoteMeta(sw.HttpSignatureParameterCreated)) + fmt.Fprintf(&sb, regexp.QuoteMeta(HttpSignatureParameterCreated)) } fmt.Fprintf(&sb, `",signature="[a-zA-Z0-9+/]+="`) re := regexp.MustCompile(sb.String()) @@ -320,12 +370,12 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *sw.HttpSignatureAuth, ex func TestHttpSignatureAuth(t *testing.T) { // Test with 'hs2019' signature scheme, and default signature algorithm (RSA SSA PKCS1.5) - authConfig := sw.HttpSignatureAuth{ + authConfig := HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. - sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. "Date", // The date and time at which the message was originated. "Content-Type", // The Media type of the body of the request. @@ -335,85 +385,85 @@ func TestHttpSignatureAuth(t *testing.T) { executeHttpSignatureAuth(t, &authConfig, true) // Test with duplicate headers. This is invalid and should be rejected. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignedHeaders: []string{"Host", "Date", "Host"}, } executeHttpSignatureAuth(t, &authConfig, false) // Test with non-existent header. This is invalid and should be rejected. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignedHeaders: []string{"Host", "Date", "Garbage-HeaderDoesNotExist"}, } executeHttpSignatureAuth(t, &authConfig, false) // Test with 'Authorization' header in the signed headers. This is invalid and should be rejected. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignedHeaders: []string{"Host", "Authorization"}, } executeHttpSignatureAuth(t, &authConfig, false) // Specify signature max validity, but '(expires)' parameter is missing. This should cause an error. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignatureMaxValidity: 7 * time.Minute, } executeHttpSignatureAuth(t, &authConfig, false) // Specify invalid signature max validity. This should cause an error. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignatureMaxValidity: -3 * time.Minute, } executeHttpSignatureAuth(t, &authConfig, false) // Specify signature max validity and '(expires)' parameter. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignatureMaxValidity: time.Minute, SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. - sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. - sw.HttpSignatureParameterExpires, // Time when signature expires. + HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + HttpSignatureParameterExpires, // Time when signature expires. }, } executeHttpSignatureAuth(t, &authConfig, true) // Specify '(expires)' parameter but no signature max validity. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. - sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. - sw.HttpSignatureParameterExpires, // Time when signature expires. + HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + HttpSignatureParameterExpires, // Time when signature expires. }, } executeHttpSignatureAuth(t, &authConfig, false) // Test with empty signed headers. The client should automatically add the "(created)" parameter by default. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, + SigningScheme: HttpSigningSchemeHs2019, SignedHeaders: []string{}, } executeHttpSignatureAuth(t, &authConfig, true) // Test with deprecated RSA-SHA512, some servers may still support it. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeRsaSha512, + SigningScheme: HttpSigningSchemeRsaSha512, SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. - sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. "Date", // The date and time at which the message was originated. "Content-Type", // The Media type of the body of the request. @@ -423,12 +473,12 @@ func TestHttpSignatureAuth(t *testing.T) { executeHttpSignatureAuth(t, &authConfig, true) // Test with deprecated RSA-SHA256, some servers may still support it. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeRsaSha256, + SigningScheme: HttpSigningSchemeRsaSha256, SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. - sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. "Date", // The date and time at which the message was originated. "Content-Type", // The Media type of the body of the request. @@ -439,12 +489,12 @@ func TestHttpSignatureAuth(t *testing.T) { // Test with headers without date. This makes it possible to get a fixed signature, used for unit test purpose. // This should not be used in production code as it could potentially allow replay attacks. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, - SigningAlgorithm: sw.HttpSigningAlgorithmRsaPKCS1v15, + SigningScheme: HttpSigningSchemeHs2019, + SigningAlgorithm: HttpSigningAlgorithmRsaPKCS1v15, SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, + HttpSignatureParameterRequestTarget, "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. "Content-Type", // The Media type of the body of the request. "Digest", // A cryptographic digest of the request body. @@ -461,12 +511,12 @@ func TestHttpSignatureAuth(t *testing.T) { } // Test with PSS signature. The PSS signature creates a new signature every time it is invoked. - authConfig = sw.HttpSignatureAuth{ + authConfig = HttpSignatureAuth{ KeyId: "my-key-id", - SigningScheme: sw.HttpSigningSchemeHs2019, - SigningAlgorithm: sw.HttpSigningAlgorithmRsaPSS, + SigningScheme: HttpSigningSchemeHs2019, + SigningAlgorithm: HttpSigningAlgorithmRsaPSS, SignedHeaders: []string{ - sw.HttpSignatureParameterRequestTarget, + HttpSignatureParameterRequestTarget, "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. "Content-Type", // The Media type of the body of the request. "Digest", // A cryptographic digest of the request body. diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go index 4d5b399f8464..600d409c3a07 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go @@ -10,6 +10,8 @@ package openapi import ( + "bytes" + "context" "crypto" "crypto/ecdsa" "crypto/rand" @@ -17,9 +19,13 @@ import ( "crypto/x509" "encoding/base64" "encoding/pem" + "fmt" + "io" "io/ioutil" + "net/http" "net/textproto" "os" + "strings" "time" ) @@ -115,7 +121,7 @@ type HttpSignatureAuth struct { // are invalid. func (h *HttpSignatureAuth) ContextWithValue(ctx context.Context) (context.Context, error) { if err := h.loadPrivateKey(); err != nil { - return err + return nil, err } return context.WithValue(ctx, ContextHttpSignatureAuth, *h), nil } @@ -125,7 +131,7 @@ func (h *HttpSignatureAuth) loadPrivateKey() (err error) { var file *os.File file, err = os.Open(h.PrivateKeyPath) if err != nil { - return err + return fmt.Errorf("Cannot load private key '%s'. Error: %v", h.PrivateKeyPath, err) } defer func() { err = file.Close() @@ -146,7 +152,7 @@ func (h *HttpSignatureAuth) loadPrivateKey() (err error) { var privKey []byte if x509.IsEncryptedPEMBlock(pemBlock) { // The PEM data is encrypted. - privKey, err = x509.DecryptPEMBlock(pemBlock, h.Passphrase) + privKey, err = x509.DecryptPEMBlock(pemBlock, []byte(h.Passphrase)) if err != nil { // Failed to decrypt PEM block. Because of deficiencies in the encrypted-PEM format, // it's not always possibleto detect an incorrect password. @@ -189,7 +195,7 @@ func SignRequest( auth HttpSignatureAuth) error { if auth.privateKey == nil { - return errors.New("Private key is not set") + return fmt.Errorf("Private key is not set") } now := time.Now() date := now.UTC().Format(http.TimeFormat) From 88da56b43e7dbdd32afa7a759a977f505ddd455d Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Wed, 22 Jan 2020 20:32:40 -0800 Subject: [PATCH 45/48] add more validation and unit tests --- .../http_signature_test.mustache | 79 ++++++++++++++++++- .../go-experimental/signing.mustache | 29 +++++++ .../go-petstore/http_signature_test.go | 79 ++++++++++++++++++- .../go-experimental/go-petstore/signing.go | 29 +++++++ 4 files changed, 214 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache b/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache index 02c7a0cb3753..f9b4611d77d3 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache @@ -262,9 +262,13 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *HttpSignatureAuth, expec authConfig.PrivateKeyPath = privateKeyPath var authCtx context.Context authCtx, err = authConfig.ContextWithValue(context.Background()) - if err != nil { + if expectSuccess && err != nil { t.Fatalf("Error validating HTTP signature configuration: %v", err) } + if !expectSuccess && err != nil { + // Do not continue. Error is expected. + return "" + } newPet := (Pet{Id: PtrInt64(12992), Name: "gopher", PhotoUrls: []string{"http://1.com", "http://2.com"}, Status: PtrString("pending"), @@ -525,3 +529,76 @@ func TestHttpSignatureAuth(t *testing.T) { t.Errorf("Authorization header value is incorrect. Got\n'%s'\nbut expected\n'%s'", authorizationHeaderValue, expectedAuthorizationHeader) } } + +func TestInvalidHttpSignatureConfiguration(t *testing.T) { + var err error + var authConfig HttpSignatureAuth + + authConfig = HttpSignatureAuth{ + } + _, err = authConfig.ContextWithValue(context.Background()) + if err == nil || !strings.Contains(err.Error(), "Key ID must be specified") { + t.Fatalf("Invalid configuration: %v", err) + } + + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + } + _, err = authConfig.ContextWithValue(context.Background()) + if err == nil || !strings.Contains(err.Error(), "Private key path must be specified") { + t.Fatalf("Invalid configuration: %v", err) + } + + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: "test.pem", + } + _, err = authConfig.ContextWithValue(context.Background()) + if err == nil || !strings.Contains(err.Error(), "Invalid signing scheme") { + t.Fatalf("Invalid configuration: %v", err) + } + + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: "test.pem", + SigningScheme: "garbage", + } + _, err = authConfig.ContextWithValue(context.Background()) + if err == nil || !strings.Contains(err.Error(), "Invalid signing scheme") { + t.Fatalf("Invalid configuration: %v", err) + } + + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: "test.pem", + SigningScheme: HttpSigningSchemeHs2019, + SignedHeaders: []string{"foo", "bar", "foo"}, + } + _, err = authConfig.ContextWithValue(context.Background()) + if err == nil || !strings.Contains(err.Error(), "cannot have duplicate names") { + t.Fatalf("Invalid configuration: %v", err) + } + + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: "test.pem", + SigningScheme: HttpSigningSchemeHs2019, + SignedHeaders: []string{"foo", "bar", "Authorization"}, + } + _, err = authConfig.ContextWithValue(context.Background()) + if err == nil || !strings.Contains(err.Error(), "Signed headers cannot include the 'Authorization' header") { + t.Fatalf("Invalid configuration: %v", err) + } + + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: "test.pem", + SigningScheme: HttpSigningSchemeHs2019, + SignedHeaders: []string{"foo", "bar"}, + SignatureMaxValidity: -7 * time.Minute, + } + _, err = authConfig.ContextWithValue(context.Background()) + if err == nil || !strings.Contains(err.Error(), "Signature max validity must be a positive value") { + t.Fatalf("Invalid configuration: %v", err) + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache index aa5259201571..f3251e8894fd 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache @@ -68,6 +68,13 @@ const ( HttpSigningAlgorithmRsaPSS string = "RSASSA-PSS" ) +var supportedSigningSchemes = map[string]bool{ + HttpSigningSchemeHs2019: true, + HttpSigningSchemeRsaSha512: true, + HttpSigningSchemeRsaSha256: true, +} + + // HttpSignatureAuth provides HTTP signature authentication to a request passed // via context using ContextHttpSignatureAuth. // An 'Authorization' header is calculated by creating a hash of select headers, @@ -112,6 +119,28 @@ type HttpSignatureAuth struct { // suitable for HTTP signature. An error is returned if the HttpSignatureAuth configuration parameters // are invalid. func (h *HttpSignatureAuth) ContextWithValue(ctx context.Context) (context.Context, error) { + if h.KeyId == "" { + return nil, fmt.Errorf("Key ID must be specified") + } + if h.PrivateKeyPath == "" { + return nil, fmt.Errorf("Private key path must be specified") + } + if _, ok := supportedSigningSchemes[h.SigningScheme]; !ok { + return nil, fmt.Errorf("Invalid signing scheme: '%v'", h.SigningScheme) + } + m := make(map[string]bool) + for _, h := range h.SignedHeaders { + if strings.ToLower(h) == strings.ToLower(HttpHeaderAuthorization) { + return nil, fmt.Errorf("Signed headers cannot include the 'Authorization' header") + } + m[h] = true + } + if len(m) != len(h.SignedHeaders) { + return nil, fmt.Errorf("List of signed headers cannot have duplicate names") + } + if h.SignatureMaxValidity < 0 { + return nil, fmt.Errorf("Signature max validity must be a positive value") + } if err := h.loadPrivateKey(); err != nil { return nil, err } diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go index 6c92fa979c29..85af43ae81ed 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go @@ -270,9 +270,13 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *HttpSignatureAuth, expec authConfig.PrivateKeyPath = privateKeyPath var authCtx context.Context authCtx, err = authConfig.ContextWithValue(context.Background()) - if err != nil { + if expectSuccess && err != nil { t.Fatalf("Error validating HTTP signature configuration: %v", err) } + if !expectSuccess && err != nil { + // Do not continue. Error is expected. + return "" + } newPet := (Pet{Id: PtrInt64(12992), Name: "gopher", PhotoUrls: []string{"http://1.com", "http://2.com"}, Status: PtrString("pending"), @@ -533,3 +537,76 @@ func TestHttpSignatureAuth(t *testing.T) { t.Errorf("Authorization header value is incorrect. Got\n'%s'\nbut expected\n'%s'", authorizationHeaderValue, expectedAuthorizationHeader) } } + +func TestInvalidHttpSignatureConfiguration(t *testing.T) { + var err error + var authConfig HttpSignatureAuth + + authConfig = HttpSignatureAuth{ + } + _, err = authConfig.ContextWithValue(context.Background()) + if err == nil || !strings.Contains(err.Error(), "Key ID must be specified") { + t.Fatalf("Invalid configuration: %v", err) + } + + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + } + _, err = authConfig.ContextWithValue(context.Background()) + if err == nil || !strings.Contains(err.Error(), "Private key path must be specified") { + t.Fatalf("Invalid configuration: %v", err) + } + + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: "test.pem", + } + _, err = authConfig.ContextWithValue(context.Background()) + if err == nil || !strings.Contains(err.Error(), "Invalid signing scheme") { + t.Fatalf("Invalid configuration: %v", err) + } + + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: "test.pem", + SigningScheme: "garbage", + } + _, err = authConfig.ContextWithValue(context.Background()) + if err == nil || !strings.Contains(err.Error(), "Invalid signing scheme") { + t.Fatalf("Invalid configuration: %v", err) + } + + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: "test.pem", + SigningScheme: HttpSigningSchemeHs2019, + SignedHeaders: []string{"foo", "bar", "foo"}, + } + _, err = authConfig.ContextWithValue(context.Background()) + if err == nil || !strings.Contains(err.Error(), "cannot have duplicate names") { + t.Fatalf("Invalid configuration: %v", err) + } + + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: "test.pem", + SigningScheme: HttpSigningSchemeHs2019, + SignedHeaders: []string{"foo", "bar", "Authorization"}, + } + _, err = authConfig.ContextWithValue(context.Background()) + if err == nil || !strings.Contains(err.Error(), "Signed headers cannot include the 'Authorization' header") { + t.Fatalf("Invalid configuration: %v", err) + } + + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: "test.pem", + SigningScheme: HttpSigningSchemeHs2019, + SignedHeaders: []string{"foo", "bar"}, + SignatureMaxValidity: -7 * time.Minute, + } + _, err = authConfig.ContextWithValue(context.Background()) + if err == nil || !strings.Contains(err.Error(), "Signature max validity must be a positive value") { + t.Fatalf("Invalid configuration: %v", err) + } +} diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go index 600d409c3a07..6a3f89b5b78a 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go @@ -76,6 +76,13 @@ const ( HttpSigningAlgorithmRsaPSS string = "RSASSA-PSS" ) +var supportedSigningSchemes = map[string]bool{ + HttpSigningSchemeHs2019: true, + HttpSigningSchemeRsaSha512: true, + HttpSigningSchemeRsaSha256: true, +} + + // HttpSignatureAuth provides HTTP signature authentication to a request passed // via context using ContextHttpSignatureAuth. // An 'Authorization' header is calculated by creating a hash of select headers, @@ -120,6 +127,28 @@ type HttpSignatureAuth struct { // suitable for HTTP signature. An error is returned if the HttpSignatureAuth configuration parameters // are invalid. func (h *HttpSignatureAuth) ContextWithValue(ctx context.Context) (context.Context, error) { + if h.KeyId == "" { + return nil, fmt.Errorf("Key ID must be specified") + } + if h.PrivateKeyPath == "" { + return nil, fmt.Errorf("Private key path must be specified") + } + if _, ok := supportedSigningSchemes[h.SigningScheme]; !ok { + return nil, fmt.Errorf("Invalid signing scheme: '%v'", h.SigningScheme) + } + m := make(map[string]bool) + for _, h := range h.SignedHeaders { + if strings.ToLower(h) == strings.ToLower(HttpHeaderAuthorization) { + return nil, fmt.Errorf("Signed headers cannot include the 'Authorization' header") + } + m[h] = true + } + if len(m) != len(h.SignedHeaders) { + return nil, fmt.Errorf("List of signed headers cannot have duplicate names") + } + if h.SignatureMaxValidity < 0 { + return nil, fmt.Errorf("Signature max validity must be a positive value") + } if err := h.loadPrivateKey(); err != nil { return nil, err } From 816aebcb3ccfc044ac076666c89bc0149ebabdd5 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Thu, 23 Jan 2020 11:51:04 -0800 Subject: [PATCH 46/48] Add helper function to return public key, and add more unit tests for signature validation --- .../http_signature_test.mustache | 116 ++++++++++++++++++ .../go-experimental/signing.mustache | 19 +++ .../go-petstore/http_signature_test.go | 116 ++++++++++++++++++ .../go-experimental/go-petstore/signing.go | 19 +++ 4 files changed, 270 insertions(+) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache b/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache index f9b4611d77d3..79202f6f4aff 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache @@ -4,12 +4,18 @@ package {{packageName}} import ( "bytes" "context" + "crypto" + "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/x509" + "encoding/asn1" + "encoding/base64" "encoding/pem" "fmt" "io/ioutil" + "math/big" + "net/http" "net/http/httputil" "os" "path/filepath" @@ -361,9 +367,119 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *HttpSignatureAuth, expec if !re.MatchString(actual) { t.Errorf("Authorization header is incorrect. Expected regex\n'%s'\nbut got\n'%s'", sb.String(), actual) } + + validateHttpAuthorizationSignature(t, authConfig, r) return r.Request.Header.Get("Authorization") } +var ( + // signatureRe is a regular expression to capture the fields from the signature. + signatureRe = regexp.MustCompile( + `Signature keyId="(?P[^"]+)",algorithm="(?P[^"]+)"` + + `(,created=(?P[0-9]+))?(,expires=(?P[0-9.]+))?,headers="(?P[^"]+)",signature="(?P[^"]+)"`) +) + +// validateHttpAuthorizationSignature validates the HTTP signature in the HTTP request. +// The signature verification would normally be done by the server. +// Note: this is NOT a complete implementation of the HTTP signature validation. This code is for unit test purpose, do not use +// it for server side implementation. +// In particular, this code does not validate the calculation of the HTTP body digest. +func validateHttpAuthorizationSignature(t *testing.T, authConfig *HttpSignatureAuth, r *http.Response) { + authHeader := r.Request.Header.Get("Authorization") + match := signatureRe.FindStringSubmatch(authHeader) + if len(match) < 3 { + t.Fatalf("Unexpected Authorization header: %s", authHeader) + } + result := make(map[string]string) + for i, name := range signatureRe.SubexpNames() { + if i != 0 && name != "" { + result[name] = match[i] + } + } + b64signature := result["signature"] + fmt.Printf("Algorithm: '%s' Headers: '%s' b64signature: '%s'\n", result["algorithm"], result["headers"], b64signature) + var sb bytes.Buffer + fmt.Fprintf(&sb, "%s %s", strings.ToLower(r.Request.Method), r.Request.URL.EscapedPath()) + if r.Request.URL.RawQuery != "" { + // The ":path" pseudo-header field includes the path and query parts + // of the target URI (the "path-absolute" production and optionally a + // '?' character followed by the "query" production (see Sections 3.3 + // and 3.4 of [RFC3986] + fmt.Fprintf(&sb, "?%s", r.Request.URL.RawQuery) + } + requestTarget := sb.String() + + var signedHeaderKvs []string + signedHeaders := strings.Split(result["headers"], " ") + for _, h := range signedHeaders { + var value string + switch h { + case HttpSignatureParameterRequestTarget: + value = requestTarget + case HttpSignatureParameterCreated: + value = result["created"] + case HttpSignatureParameterExpires: + value = result["expires"] + default: + value = r.Request.Header.Get(h) + } + signedHeaderKvs = append(signedHeaderKvs, fmt.Sprintf("%s: %s", h, value)) + } + stringToSign := strings.Join(signedHeaderKvs, "\n") + + var h crypto.Hash + switch result["algorithm"] { + case HttpSigningSchemeHs2019, HttpSigningSchemeRsaSha512: + h = crypto.SHA512 + case HttpSigningSchemeRsaSha256: + h = crypto.SHA256 + default: + t.Fatalf("Unexpected signing algorithm: %s", result["algorithm"]) + } + msgHash := h.New() + if _, err := msgHash.Write([]byte(stringToSign)); err != nil { + t.Fatalf("Unable to compute hash: %v", err) + } + d := msgHash.Sum(nil) + var pub crypto.PublicKey + var err error + if pub, err = authConfig.GetPublicKey(); err != nil { + t.Fatalf("Unable to get public key: %v", err) + } + if pub == nil { + t.Fatalf("Public key is nil") + } + var signature []byte + if signature, err = base64.StdEncoding.DecodeString(b64signature); err != nil { + t.Fatalf("Failed to decode signature: %v", err) + } + switch publicKey := pub.(type) { + case *rsa.PublicKey: + // It could be PKCS1v15 or PSS signature + var errPKCS1v15, errPSS error + // In a server-side implementation, we wouldn't try to validate both signatures. The specific + // signature algorithm would be derived from the key id. But here we just want to validate for unit test purpose. + errPKCS1v15 = rsa.VerifyPKCS1v15(publicKey, h, d, signature) + errPSS = rsa.VerifyPSS(publicKey, h, d, signature, nil) + if errPKCS1v15 != nil && errPSS != nil { + t.Fatalf("RSA Signature verification failed: %v. %v", errPKCS1v15, errPSS) + } + case *ecdsa.PublicKey: + type ecdsaSignature struct { + R, S *big.Int + } + var lEcdsa ecdsaSignature + if _, err = asn1.Unmarshal(signature, &lEcdsa); err != nil { + t.Fatalf("Unable to parse ECDSA signature: %v", err) + } + if !ecdsa.Verify(publicKey, d, lEcdsa.R, lEcdsa.S) { + t.Fatalf("ECDSA Signature verification failed") + } + default: + t.Fatalf("Unsupported public key: %T", pub) + } +} + func TestHttpSignatureAuth(t *testing.T) { // Test with 'hs2019' signature scheme, and default signature algorithm (RSA SSA PKCS1.5) authConfig := HttpSignatureAuth{ diff --git a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache index f3251e8894fd..47b028504f69 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache @@ -147,6 +147,25 @@ func (h *HttpSignatureAuth) ContextWithValue(ctx context.Context) (context.Conte return context.WithValue(ctx, ContextHttpSignatureAuth, *h), nil } +// GetPublicKey returns the public key associated with this HTTP signature configuration. +func (h *HttpSignatureAuth) GetPublicKey() (crypto.PublicKey, error) { + if h.privateKey == nil { + if err := h.loadPrivateKey(); err != nil { + return nil, err + } + } + switch key := h.privateKey.(type) { + case *rsa.PrivateKey: + return key.Public(), nil + case *ecdsa.PrivateKey: + return key.Public(), nil + default: + // Do not change '%T' to anything else such as '%v'! + // The value of the private key must not be returned. + return nil, fmt.Errorf("Unsupported key: %T", h.privateKey) + } +} + // loadPrivateKey reads the private key from the file specified in the HttpSignatureAuth. func (h *HttpSignatureAuth) loadPrivateKey() (err error) { var file *os.File diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go index 85af43ae81ed..8826ad905992 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go @@ -12,12 +12,18 @@ package openapi import ( "bytes" "context" + "crypto" + "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/x509" + "encoding/asn1" + "encoding/base64" "encoding/pem" "fmt" "io/ioutil" + "math/big" + "net/http" "net/http/httputil" "os" "path/filepath" @@ -369,9 +375,119 @@ func executeHttpSignatureAuth(t *testing.T, authConfig *HttpSignatureAuth, expec if !re.MatchString(actual) { t.Errorf("Authorization header is incorrect. Expected regex\n'%s'\nbut got\n'%s'", sb.String(), actual) } + + validateHttpAuthorizationSignature(t, authConfig, r) return r.Request.Header.Get("Authorization") } +var ( + // signatureRe is a regular expression to capture the fields from the signature. + signatureRe = regexp.MustCompile( + `Signature keyId="(?P[^"]+)",algorithm="(?P[^"]+)"` + + `(,created=(?P[0-9]+))?(,expires=(?P[0-9.]+))?,headers="(?P[^"]+)",signature="(?P[^"]+)"`) +) + +// validateHttpAuthorizationSignature validates the HTTP signature in the HTTP request. +// The signature verification would normally be done by the server. +// Note: this is NOT a complete implementation of the HTTP signature validation. This code is for unit test purpose, do not use +// it for server side implementation. +// In particular, this code does not validate the calculation of the HTTP body digest. +func validateHttpAuthorizationSignature(t *testing.T, authConfig *HttpSignatureAuth, r *http.Response) { + authHeader := r.Request.Header.Get("Authorization") + match := signatureRe.FindStringSubmatch(authHeader) + if len(match) < 3 { + t.Fatalf("Unexpected Authorization header: %s", authHeader) + } + result := make(map[string]string) + for i, name := range signatureRe.SubexpNames() { + if i != 0 && name != "" { + result[name] = match[i] + } + } + b64signature := result["signature"] + fmt.Printf("Algorithm: '%s' Headers: '%s' b64signature: '%s'\n", result["algorithm"], result["headers"], b64signature) + var sb bytes.Buffer + fmt.Fprintf(&sb, "%s %s", strings.ToLower(r.Request.Method), r.Request.URL.EscapedPath()) + if r.Request.URL.RawQuery != "" { + // The ":path" pseudo-header field includes the path and query parts + // of the target URI (the "path-absolute" production and optionally a + // '?' character followed by the "query" production (see Sections 3.3 + // and 3.4 of [RFC3986] + fmt.Fprintf(&sb, "?%s", r.Request.URL.RawQuery) + } + requestTarget := sb.String() + + var signedHeaderKvs []string + signedHeaders := strings.Split(result["headers"], " ") + for _, h := range signedHeaders { + var value string + switch h { + case HttpSignatureParameterRequestTarget: + value = requestTarget + case HttpSignatureParameterCreated: + value = result["created"] + case HttpSignatureParameterExpires: + value = result["expires"] + default: + value = r.Request.Header.Get(h) + } + signedHeaderKvs = append(signedHeaderKvs, fmt.Sprintf("%s: %s", h, value)) + } + stringToSign := strings.Join(signedHeaderKvs, "\n") + + var h crypto.Hash + switch result["algorithm"] { + case HttpSigningSchemeHs2019, HttpSigningSchemeRsaSha512: + h = crypto.SHA512 + case HttpSigningSchemeRsaSha256: + h = crypto.SHA256 + default: + t.Fatalf("Unexpected signing algorithm: %s", result["algorithm"]) + } + msgHash := h.New() + if _, err := msgHash.Write([]byte(stringToSign)); err != nil { + t.Fatalf("Unable to compute hash: %v", err) + } + d := msgHash.Sum(nil) + var pub crypto.PublicKey + var err error + if pub, err = authConfig.GetPublicKey(); err != nil { + t.Fatalf("Unable to get public key: %v", err) + } + if pub == nil { + t.Fatalf("Public key is nil") + } + var signature []byte + if signature, err = base64.StdEncoding.DecodeString(b64signature); err != nil { + t.Fatalf("Failed to decode signature: %v", err) + } + switch publicKey := pub.(type) { + case *rsa.PublicKey: + // It could be PKCS1v15 or PSS signature + var errPKCS1v15, errPSS error + // In a server-side implementation, we wouldn't try to validate both signatures. The specific + // signature algorithm would be derived from the key id. But here we just want to validate for unit test purpose. + errPKCS1v15 = rsa.VerifyPKCS1v15(publicKey, h, d, signature) + errPSS = rsa.VerifyPSS(publicKey, h, d, signature, nil) + if errPKCS1v15 != nil && errPSS != nil { + t.Fatalf("RSA Signature verification failed: %v. %v", errPKCS1v15, errPSS) + } + case *ecdsa.PublicKey: + type ecdsaSignature struct { + R, S *big.Int + } + var lEcdsa ecdsaSignature + if _, err = asn1.Unmarshal(signature, &lEcdsa); err != nil { + t.Fatalf("Unable to parse ECDSA signature: %v", err) + } + if !ecdsa.Verify(publicKey, d, lEcdsa.R, lEcdsa.S) { + t.Fatalf("ECDSA Signature verification failed") + } + default: + t.Fatalf("Unsupported public key: %T", pub) + } +} + func TestHttpSignatureAuth(t *testing.T) { // Test with 'hs2019' signature scheme, and default signature algorithm (RSA SSA PKCS1.5) authConfig := HttpSignatureAuth{ diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go index 6a3f89b5b78a..73d6ed0ba961 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go @@ -155,6 +155,25 @@ func (h *HttpSignatureAuth) ContextWithValue(ctx context.Context) (context.Conte return context.WithValue(ctx, ContextHttpSignatureAuth, *h), nil } +// GetPublicKey returns the public key associated with this HTTP signature configuration. +func (h *HttpSignatureAuth) GetPublicKey() (crypto.PublicKey, error) { + if h.privateKey == nil { + if err := h.loadPrivateKey(); err != nil { + return nil, err + } + } + switch key := h.privateKey.(type) { + case *rsa.PrivateKey: + return key.Public(), nil + case *ecdsa.PrivateKey: + return key.Public(), nil + default: + // Do not change '%T' to anything else such as '%v'! + // The value of the private key must not be returned. + return nil, fmt.Errorf("Unsupported key: %T", h.privateKey) + } +} + // loadPrivateKey reads the private key from the file specified in the HttpSignatureAuth. func (h *HttpSignatureAuth) loadPrivateKey() (err error) { var file *os.File From b8be1bc3e22968bad22a90e4d8e9d6a261a937b8 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Thu, 23 Jan 2020 22:15:19 -0800 Subject: [PATCH 47/48] run sample scripts --- .../go-experimental/go-petstore/http_signature_test.go | 4 ++-- .../client/petstore/go-experimental/go-petstore/signing.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go index 8826ad905992..f5f08b2ae6ef 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go @@ -7,7 +7,7 @@ * Generated by: OpenAPI Generator (https://openapi-generator.tech) */ -package openapi +package petstore import ( "bytes" @@ -725,4 +725,4 @@ func TestInvalidHttpSignatureConfiguration(t *testing.T) { if err == nil || !strings.Contains(err.Error(), "Signature max validity must be a positive value") { t.Fatalf("Invalid configuration: %v", err) } -} +} \ No newline at end of file diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go index 73d6ed0ba961..158f92ef99dc 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go @@ -7,7 +7,7 @@ * Generated by: OpenAPI Generator (https://openapi-generator.tech) */ -package openapi +package petstore import ( "bytes" From 17952bfe58b050cf790e13e5919af9775efcdba1 Mon Sep 17 00:00:00 2001 From: "Sebastien Rosset (serosset)" Date: Fri, 24 Jan 2020 12:16:14 -0800 Subject: [PATCH 48/48] some people save their private key with file extensions other than .pem, so I am relaxing the validation of the private key suffix --- .../src/main/resources/go-experimental/signing.mustache | 3 --- .../client/petstore/go-experimental/go-petstore/signing.go | 3 --- 2 files changed, 6 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache index 47b028504f69..0dbfd0e5fa44 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache @@ -181,9 +181,6 @@ func (h *HttpSignatureAuth) loadPrivateKey() (err error) { if err != nil { return err } - if !strings.HasSuffix(h.PrivateKeyPath, ".pem") { - return fmt.Errorf("Private key must be in PEM format.") - } pemBlock, _ := pem.Decode(priv) if pemBlock == nil { // No PEM data has been found. diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go b/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go index 158f92ef99dc..d28fd6a379e3 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go @@ -189,9 +189,6 @@ func (h *HttpSignatureAuth) loadPrivateKey() (err error) { if err != nil { return err } - if !strings.HasSuffix(h.PrivateKeyPath, ".pem") { - return fmt.Errorf("Private key must be in PEM format.") - } pemBlock, _ := pem.Decode(priv) if pemBlock == nil { // No PEM data has been found.