From 824acce64311422ff49973ada44987700901f509 Mon Sep 17 00:00:00 2001 From: Prabhjot Singh Sethi Date: Wed, 28 May 2025 11:42:55 +0000 Subject: [PATCH] Enable Generator and Validator functionality Signed-off-by: Prabhjot Singh Sethi --- hash/generator.go | 122 ++++++++++++++++++++++++++++++--- hash/validator.go | 148 +++++++++++++++++++++++++++++++++++++++++ hash/validator_test.go | 49 ++++++++++++++ 3 files changed, 308 insertions(+), 11 deletions(-) create mode 100644 hash/validator.go create mode 100644 hash/validator_test.go diff --git a/hash/generator.go b/hash/generator.go index 96246ce..5cad3b4 100644 --- a/hash/generator.go +++ b/hash/generator.go @@ -7,17 +7,24 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/hex" + "net/http" "strings" + "time" ) /* -Package hash provides utilities for generating cryptographic hashes. +Package hash provides utilities for generating cryptographic hashes and +signing HTTP requests with HMAC-based authentication headers. -This file contains a function to generate a SHA-256 HMAC (Hash-based Message Authentication Code) -from a secret and one or more input strings. +This file contains: +- Functions to generate a SHA-256 HMAC (Hash-based Message Authentication Code) + from a secret and one or more input strings. +- An interface and implementation for attaching authentication headers to HTTP requests. # Usage +Basic HMAC Generation: + import ( "fmt" "yourmodule/hash" @@ -30,6 +37,20 @@ from a secret and one or more input strings. fmt.Println("HMAC:", signature) } +Signing HTTP Requests: + + import ( + "net/http" + "yourmodule/hash" + ) + + func main() { + gen := hash.NewGenerator("api-key-id", "supersecret") + req, _ := http.NewRequest("GET", "https://api.example.com/resource", nil) + signedReq := gen.AddAuthHeaders(req) + // signedReq now contains x-signature, x-api-key-id, and x-timestamp headers + } + # Function Details - GenerateSHA256HMAC(secret string, v ...string) string @@ -38,8 +59,36 @@ from a secret and one or more input strings. - v: Variadic string arguments to be concatenated and signed. Returns a hex-encoded string representing the HMAC-SHA256 signature. + +- Generator interface + + - AddAuthHeaders(r *http.Request) *http.Request + Adds authentication headers to the provided HTTP request. + +- NewGenerator(id, secret string) Generator + + - id: API key identifier. + - secret: Secret key for HMAC signing. + + Returns a Generator instance for signing HTTP requests. */ +// generateSHA256HMAC computes the raw SHA-256 HMAC for the concatenated input strings using the provided secret key. +// Returns the HMAC as a byte slice (not hex-encoded). +func generateSHA256HMAC(secret string, v ...string) []byte { + // Concatenate all input strings into a single string + raw := strings.Join(v, "") + + // Create a new HMAC hasher using SHA-256 and the provided secret key + h := hmac.New(sha256.New, []byte(secret)) + + // Write the concatenated string to the hasher + h.Write([]byte(raw)) + + // Return the raw HMAC bytes + return h.Sum(nil) +} + // GenerateSHA256HMAC generates a SHA-256 HMAC signature for the given input strings using the provided secret key. // The input strings are concatenated in the order provided, and the resulting string is signed. // Returns the signature as a hex-encoded string. @@ -53,15 +102,66 @@ from a secret and one or more input strings. // sig := GenerateSHA256HMAC("mysecret", "foo", "bar") // // sig now contains the hex-encoded HMAC of "foobar" using "mysecret" as the key. func GenerateSHA256HMAC(secret string, v ...string) string { - // Concatenate all input strings into a single string - raw := strings.Join(v, "") + // Compute the HMAC and return it as a hex-encoded string + return hex.EncodeToString(generateSHA256HMAC(secret, v...)) +} - // Create a new HMAC hasher using SHA-256 and the provided secret key - h := hmac.New(sha256.New, []byte(secret)) +// Generator defines an interface for adding authentication headers to HTTP requests. +// Implementations should add at least a signature, API key ID, and timestamp. +type Generator interface { + // AddAuthHeaders adds authentication headers to the provided HTTP request and returns it. + AddAuthHeaders(r *http.Request) *http.Request +} - // Write the concatenated string to the hasher - h.Write([]byte(raw)) +// generator is a concrete implementation of the Generator interface. +// It holds the API key ID and secret used for signing requests. +type generator struct { + id string // API key identifier + secret string // Secret key for HMAC signing +} - // Compute the HMAC and return it as a hex-encoded string - return hex.EncodeToString(h.Sum(nil)) +// AddAuthHeaders attaches authentication headers to the given HTTP request. +// The following headers are added: +// - x-signature: HMAC-SHA256 signature of the HTTP method, path, and timestamp +// - x-api-key-id: The API key identifier +// - x-timestamp: The current timestamp in RFC3339 format +// +// The signature is computed as HMAC(secret, method + path + timestamp). +func (g *generator) AddAuthHeaders(r *http.Request) *http.Request { + // use RFC3339 format for the time stamp in the header + timeStamp := time.Now().Format(time.RFC3339) + + // Compute the signature using HTTP method, path, and timestamp + sig := GenerateSHA256HMAC(g.secret, r.Method, r.URL.Path, timeStamp) + + // Add the computed signature to the request headers + r.Header.Add("x-signature", sig) + + // Add the API key ID to the request headers + r.Header.Add("x-api-key-id", g.id) + + // add timestamp to header + r.Header.Add("x-timestamp", timeStamp) + return r +} + +// NewGenerator creates a new Generator instance for signing HTTP requests. +// +// Parameters: +// - id: API key identifier +// - secret: Secret key for HMAC signing +// +// Returns: +// - Generator: An instance that can add authentication headers to HTTP requests. +// +// Example: +// +// gen := hash.NewGenerator("api-key-id", "supersecret") +// req, _ := http.NewRequest("GET", "https://api.example.com/resource", nil) +// signedReq := gen.AddAuthHeaders(req) +func NewGenerator(id, secret string) Generator { + return &generator{ + id: id, + secret: secret, + } } diff --git a/hash/validator.go b/hash/validator.go new file mode 100644 index 0000000..ac54191 --- /dev/null +++ b/hash/validator.go @@ -0,0 +1,148 @@ +// Copyright © 2025 Prabhjot Singh Sethi, All Rights reserved +// Author: Prabhjot Singh Sethi +// +// This file is part of the hash package, which provides cryptographic utilities +// for validating HMAC-SHA256 signatures and authenticating HTTP requests. + +package hash + +import ( + "crypto/hmac" + "encoding/hex" + "fmt" + "net/http" + "time" +) + +/* +Package hash provides utilities for validating cryptographic signatures +on HTTP requests using HMAC-SHA256. + +This file contains: +- An interface and implementation for validating authentication headers on HTTP requests. +- Logic to check the signature, timestamp, and expiration of requests. + +# Usage + + import ( + "net/http" + "yourmodule/hash" + ) + + func main() { + // validity is the allowed time window (in seconds) for a request to be considered valid + validator := hash.NewValidator(60) // 60 seconds validity + + // Assume req is an *http.Request with authentication headers + ok, err := validator.Validate(req, "supersecret") + if !ok { + fmt.Println("Validation failed:", err) + return + } + fmt.Println("Request is valid!") + } + +# Function Details + +- Validator interface + + - Validate(r *http.Request, secret string) (bool, error) + Validates the authentication headers on the provided HTTP request. + +- NewValidator(validity int64) Validator + + - validity: Allowed time window (in seconds) for the request to be valid. + + Returns a Validator instance for validating HTTP requests. +*/ + +// Validator defines an interface for validating authentication headers on HTTP requests. +type Validator interface { + // Validate checks the HMAC signature, timestamp, and expiration of the request. + // Returns true if valid, false and an error otherwise. + Validate(r *http.Request, secret string) (bool, error) +} + +// validator is a concrete implementation of the Validator interface. +// It holds the allowed validity window (in seconds) for request timestamps. +type validator struct { + validity int64 // Allowed time window (in seconds) for request validity +} + +// Validate checks the HMAC signature, timestamp, and expiration of the HTTP request. +// +// Steps performed: +// 1. Ensures required headers are present: x-signature and x-timestamp. +// 2. Decodes the hex-encoded signature from the x-signature header. +// 3. Parses the timestamp from the x-timestamp header (RFC3339 format). +// 4. Checks if the request is within the allowed validity window. +// 5. Recomputes the expected HMAC signature and compares it to the provided signature. +// +// Parameters: +// - r: The HTTP request to validate. +// - secret: The secret key used for HMAC validation. +// +// Returns: +// - bool: true if the request is valid, false otherwise. +// - error: Reason for validation failure, if any. +func (v *validator) Validate(r *http.Request, secret string) (bool, error) { + // Ensure headers are present + if len(r.Header) == 0 { + return false, fmt.Errorf("missing required headers") + } + + // Retrieve the signature from the header + sigStr := r.Header.Get("x-signature") + if sigStr == "" { + return false, fmt.Errorf("missing signature header") + } + + // Decode the hex-encoded signature + sig, err := hex.DecodeString(sigStr) + if err != nil { + return false, fmt.Errorf("invalid signature format") + } + + // Retrieve the timestamp from the header + timeStr := r.Header.Get("x-timestamp") + if timeStr == "" { + return false, fmt.Errorf("missing timestamp header") + } + + // Parse the timestamp (RFC3339 format) + timeStamp, err := time.Parse(time.RFC3339, timeStr) + if err != nil { + return false, fmt.Errorf("error parsing timestamp: %s", err) + } + + // Check if the request is within the allowed validity window + now := time.Now().Unix() + if now >= (timeStamp.Unix() + v.validity) { + return false, fmt.Errorf("expired access") + } + + // Recompute the expected HMAC signature using the method, path, and timestamp + if !hmac.Equal(sig, generateSHA256HMAC(secret, r.Method, r.URL.Path, timeStr)) { + return false, fmt.Errorf("invalid hmac signature") + } + + return true, nil +} + +// NewValidator creates a new Validator instance for validating HTTP requests. +// +// Parameters: +// - validity: Allowed time window (in seconds) for the request to be valid. +// +// Returns: +// - Validator: An instance that can validate authentication headers on HTTP requests. +// +// Example: +// +// validator := hash.NewValidator(60) // 60 seconds validity +// ok, err := validator.Validate(req, "supersecret") +func NewValidator(validity int64) Validator { + return &validator{ + validity: validity, + } +} diff --git a/hash/validator_test.go b/hash/validator_test.go new file mode 100644 index 0000000..9597f27 --- /dev/null +++ b/hash/validator_test.go @@ -0,0 +1,49 @@ +// Copyright © 2025 Prabhjot Singh Sethi, All Rights reserved +// Author: Prabhjot Singh Sethi + +package hash + +import ( + "net/http/httptest" + "testing" + "time" +) + +// TestGeneratorAndValidator demonstrates signing an HTTP request with Generator +// and validating it with Validator. +func TestGeneratorAndValidator(t *testing.T) { + // Setup + apiKeyID := "test-key" + secret := "supersecret" + validity := int64(60) // 60 seconds validity window + + // Create a new HTTP request + req := httptest.NewRequest("GET", "https://api.example.com/resource", nil) + + // Sign the request using the Generator + gen := NewGenerator(apiKeyID, secret) + signedReq := gen.AddAuthHeaders(req) + + // Validate the signed request using the Validator + validator := NewValidator(validity) + ok, err := validator.Validate(signedReq, secret) + if !ok { + t.Fatalf("Validation failed: %v", err) + } + + // Tamper with the signature to ensure validation fails + signedReq.Header.Set("x-signature", "deadbeef") + ok, err = validator.Validate(signedReq, secret) + if ok || err == nil { + t.Fatalf("Expected validation to fail for tampered signature") + } + + // Tamper with the timestamp to simulate expiration + signedReq = gen.AddAuthHeaders(req) // re-sign to get a valid signature + oldTime := time.Now().Add(-2 * time.Minute).Format(time.RFC3339) + signedReq.Header.Set("x-timestamp", oldTime) + ok, err = validator.Validate(signedReq, secret) + if ok || err == nil || err.Error() != "expired access" { + t.Fatalf("Expected expired access error, got: %v", err) + } +}