diff --git a/bin/openapi3/go-experimental-petstore.sh b/bin/openapi3/go-experimental-petstore.sh index 46c7841a2800..901c8c62dc4e 100755 --- a/bin/openapi3/go-experimental-petstore.sh +++ b/bin/openapi3/go-experimental-petstore.sh @@ -25,7 +25,11 @@ 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" +# 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/java/org/openapitools/codegen/languages/GoClientExperimentalCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoClientExperimentalCodegen.java index 5edabbefbd15..d7fa0c435a4b 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,15 @@ 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")); + supportingFiles.add(new SupportingFile("http_signature_test.mustache", "", "http_signature_test.go")); + } } @Override 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 ca2082dc8dc7..3f037b0be5a4 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/README.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/README.mustache @@ -104,7 +104,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 @@ -116,6 +118,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/client.mustache b/modules/openapi-generator/src/main/resources/go-experimental/client.mustache index 6678caedfb85..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,18 @@ 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. + if auth, ok := ctx.Value(ContextHttpSignatureAuth).(HttpSignatureAuth); ok { + err = SignRequest(ctx, localVarRequest, auth) + if err != nil { + return nil, err + } + } + } +{{/hasHttpSignatureMethods}} 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 d7b93eac06ab..3cda1954db89 100644 --- a/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache +++ b/modules/openapi-generator/src/main/resources/go-experimental/configuration.mustache @@ -31,6 +31,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") 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..79202f6f4aff --- /dev/null +++ b/modules/openapi-generator/src/main/resources/go-experimental/http_signature_test.mustache @@ -0,0 +1,720 @@ +{{>partial_header}} +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" + "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, filePath string) { + err := ioutil.WriteFile(filePath, []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, 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(filePath) + 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 != "" { + t.Fatalf("Encrypting PKCS#1-encoded private key with passphrase is not supported") + } + privKeyBytes = x509.MarshalPKCS1PrivateKey(key) + case keyFormatPkcs8Pem: + privKeyBytes, err = x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatalf("Error writing private key: %v", err) + } + case keyFormatPkcs8Der: + if passphrase != "" { + 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(privKeyBytes) + if err != nil { + t.Fatalf("Error writing DER-encoded private key: %v", err) + } + default: + t.Fatalf("Unsupported key format: %v", format) + } + + 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) + } + } + 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, 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(filePath) + 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) { + 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, + x509.PEMCipherAES128, + x509.PEMCipherAES192, + x509.PEMCipherAES256, + } + // Test RSA private keys with various key lengths. + for _, bits := range []int{1024, 2048, 3072, 4096} { + + 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" + 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) + + 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 '%s': %v", privateKeyPath, 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) + } + } + } + } + } + + /* + Unfortunately, currently the build environment for OpenAPITools is using an old version + of golang that does not support ECDSA keys. + { + privateKeyPath := "privatekey.pem" + authConfig := 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 '%s': %v", privateKeyPath, err) + } + } + */ +} + +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 := NewAPIClient(cfg) + + privateKeyPath := filepath.Join(dir, "rsa.pem") + writeTestRsaPemKey(t, privateKeyPath) + authConfig.PrivateKeyPath = privateKeyPath + var authCtx context.Context + authCtx, err = authConfig.ContextWithValue(context.Background()) + 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"), + 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).Pet(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 == HttpSignatureParameterCreated { + fmt.Fprintf(&sb, `created=[0-9]+,`) + } + if header == 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 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(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) + } + + 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{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SignedHeaders: []string{ + 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. + "Digest", // A cryptographic digest of the request body. + }, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Test with duplicate headers. This is invalid and should be rejected. + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SignedHeaders: []string{"Host", "Date", "Host"}, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Test with non-existent header. This is invalid and should be rejected. + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + 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 = HttpSignatureAuth{ + KeyId: "my-key-id", + 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 = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SignatureMaxValidity: 7 * time.Minute, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Specify invalid signature max validity. This should cause an error. + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SignatureMaxValidity: -3 * time.Minute, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Specify signature max validity and '(expires)' parameter. + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SignatureMaxValidity: time.Minute, + SignedHeaders: []string{ + 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 = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SignedHeaders: []string{ + 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 = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SignedHeaders: []string{}, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Test with deprecated RSA-SHA512, some servers may still support it. + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeRsaSha512, + SignedHeaders: []string{ + 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. + "Digest", // A cryptographic digest of the request body. + }, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Test with deprecated RSA-SHA256, some servers may still support it. + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeRsaSha256, + SignedHeaders: []string{ + 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. + "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 = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SigningAlgorithm: HttpSigningAlgorithmRsaPKCS1v15, + SignedHeaders: []string{ + 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 = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SigningAlgorithm: HttpSigningAlgorithmRsaPSS, + SignedHeaders: []string{ + 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 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 new file mode 100644 index 000000000000..0dbfd0e5fa44 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/go-experimental/signing.mustache @@ -0,0 +1,414 @@ +{{>partial_header}} +package {{packageName}} + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/textproto" + "os" + "strings" + "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" +) + +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, +// 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 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 + } + 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 + file, err = os.Open(h.PrivateKeyPath) + if err != nil { + return fmt.Errorf("Cannot load private key '%s'. Error: %v", h.PrivateKeyPath, err) + } + defer func() { + err = file.Close() + }() + var priv []byte + priv, err = ioutil.ReadAll(file) + if err != nil { + return err + } + 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, []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. + 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 fmt.Errorf("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 +} diff --git a/samples/client/petstore/go-experimental/go-petstore/client.go b/samples/client/petstore/go-experimental/go-petstore/client.go index 4c4ecf168f41..2f97c129ab15 100644 --- a/samples/client/petstore/go-experimental/go-petstore/client.go +++ b/samples/client/petstore/go-experimental/go-petstore/client.go @@ -362,7 +362,6 @@ func (c *APIClient) prepareRequest( for header, value := range c.cfg.DefaultHeader { localVarRequest.Header.Add(header, value) } - 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..4c503f92637b 100644 --- a/samples/client/petstore/go-experimental/go-petstore/configuration.go +++ b/samples/client/petstore/go-experimental/go-petstore/configuration.go @@ -39,6 +39,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") 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 9c0050a23522..a7fa78304046 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/README.md @@ -189,6 +189,9 @@ Note, each API key must be added to a map of `map[string]APIKey` where the key i ### bearer_test + +### http_basic_test + - **Type**: HTTP basic authentication Example @@ -202,20 +205,37 @@ r, err := client.Service.Operation(auth, args) ``` -### http_basic_test +### 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 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 71d688ccea20..177b761d6b6b 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 01ee73b79ec9..9da20522a22e 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/client.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/client.go @@ -365,7 +365,16 @@ 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 = 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 a6bbfbeff964..ddcea71eaf9a 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/configuration.go +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/configuration.go @@ -39,6 +39,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") 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 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..f5f08b2ae6ef --- /dev/null +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/http_signature_test.go @@ -0,0 +1,728 @@ +/* + * 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 petstore + +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" + "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, filePath string) { + err := ioutil.WriteFile(filePath, []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, 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(filePath) + 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 != "" { + t.Fatalf("Encrypting PKCS#1-encoded private key with passphrase is not supported") + } + privKeyBytes = x509.MarshalPKCS1PrivateKey(key) + case keyFormatPkcs8Pem: + privKeyBytes, err = x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatalf("Error writing private key: %v", err) + } + case keyFormatPkcs8Der: + if passphrase != "" { + 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(privKeyBytes) + if err != nil { + t.Fatalf("Error writing DER-encoded private key: %v", err) + } + default: + t.Fatalf("Unsupported key format: %v", format) + } + + 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) + } + } + 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, 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(filePath) + 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) { + 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, + x509.PEMCipherAES128, + x509.PEMCipherAES192, + x509.PEMCipherAES256, + } + // Test RSA private keys with various key lengths. + for _, bits := range []int{1024, 2048, 3072, 4096} { + + 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" + 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) + + 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 '%s': %v", privateKeyPath, 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) + } + } + } + } + } + + /* + Unfortunately, currently the build environment for OpenAPITools is using an old version + of golang that does not support ECDSA keys. + { + privateKeyPath := "privatekey.pem" + authConfig := 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 '%s': %v", privateKeyPath, err) + } + } + */ +} + +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 := NewAPIClient(cfg) + + privateKeyPath := filepath.Join(dir, "rsa.pem") + writeTestRsaPemKey(t, privateKeyPath) + authConfig.PrivateKeyPath = privateKeyPath + var authCtx context.Context + authCtx, err = authConfig.ContextWithValue(context.Background()) + 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"), + 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).Pet(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 == HttpSignatureParameterCreated { + fmt.Fprintf(&sb, `created=[0-9]+,`) + } + if header == 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 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(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) + } + + 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{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SignedHeaders: []string{ + 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. + "Digest", // A cryptographic digest of the request body. + }, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Test with duplicate headers. This is invalid and should be rejected. + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SignedHeaders: []string{"Host", "Date", "Host"}, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Test with non-existent header. This is invalid and should be rejected. + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + 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 = HttpSignatureAuth{ + KeyId: "my-key-id", + 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 = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SignatureMaxValidity: 7 * time.Minute, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Specify invalid signature max validity. This should cause an error. + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SignatureMaxValidity: -3 * time.Minute, + } + executeHttpSignatureAuth(t, &authConfig, false) + + // Specify signature max validity and '(expires)' parameter. + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SignatureMaxValidity: time.Minute, + SignedHeaders: []string{ + 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 = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SignedHeaders: []string{ + 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 = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SignedHeaders: []string{}, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Test with deprecated RSA-SHA512, some servers may still support it. + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeRsaSha512, + SignedHeaders: []string{ + 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. + "Digest", // A cryptographic digest of the request body. + }, + } + executeHttpSignatureAuth(t, &authConfig, true) + + // Test with deprecated RSA-SHA256, some servers may still support it. + authConfig = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeRsaSha256, + SignedHeaders: []string{ + 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. + "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 = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SigningAlgorithm: HttpSigningAlgorithmRsaPKCS1v15, + SignedHeaders: []string{ + 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 = HttpSignatureAuth{ + KeyId: "my-key-id", + SigningScheme: HttpSigningSchemeHs2019, + SigningAlgorithm: HttpSigningAlgorithmRsaPSS, + SignedHeaders: []string{ + 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 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/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..d28fd6a379e3 --- /dev/null +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/signing.go @@ -0,0 +1,422 @@ +/* + * 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 petstore + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/textproto" + "os" + "strings" + "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" +) + +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, +// 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 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 + } + 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 + file, err = os.Open(h.PrivateKeyPath) + if err != nil { + return fmt.Errorf("Cannot load private key '%s'. Error: %v", h.PrivateKeyPath, err) + } + defer func() { + err = file.Close() + }() + var priv []byte + priv, err = ioutil.ReadAll(file) + if err != nil { + return err + } + 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, []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. + 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 fmt.Errorf("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 +}