package reqsig import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "errors" "strings" ) // ParseSignatureHeader parses the RFC 9421 Signature header. // Returns the key_id and a map of signature parameters. func ParseSignatureHeader(header string) (keyID string, params map[string]string, err error) { if !strings.HasPrefix(header, "Signature ") { return "", nil, errors.New("invalid signature header format") } body := strings.TrimPrefix(header, "Signature ") params = make(map[string]string) // Split by comma for parameters parts := strings.Split(body, ",") for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } // Find the first = sign eqIdx := strings.Index(part, "=") if eqIdx == -1 { continue } key := part[:eqIdx] value := part[eqIdx+1:] // Remove quotes if present if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") { value = value[1 : len(value)-1] } params[key] = value } keyID = params["key_id"] if keyID == "" { return "", nil, errors.New("missing key_id in signature") } return keyID, params, nil } // ParseSignatureInputHeader parses the Signature-Input header. // Returns a map of signature label to signed string. func ParseSignatureInputHeader(header string) (map[string]string, error) { if !strings.HasPrefix(header, "sig1=") { return nil, errors.New("invalid signature-input header format") } body := strings.TrimPrefix(header, "sig1=") if !strings.HasPrefix(body, "\"") || !strings.HasSuffix(body, "\"") { return nil, errors.New("invalid signature-input value format") } body = body[1 : len(body)-1] // Extract signed string (after "sig1=") eqIdx := strings.Index(body, "=") if eqIdx == -1 { return nil, errors.New("invalid signature-input format") } signedString := body[eqIdx+1:] // Remove quotes if present if strings.HasPrefix(signedString, "\"") && strings.HasSuffix(signedString, "\"") { signedString = signedString[1 : len(signedString)-1] } result := make(map[string]string) result["sig1"] = signedString return result, nil } // ComputeBodyHash computes the SHA256 hash of the request body. // If digest header is present, it parses and returns the hash. // Otherwise, it computes the hash from the provided body. func ComputeBodyHash(body []byte, digestHeader string) (string, error) { if digestHeader != "" { // Parse digest header: "SHA-256=base64hash" parts := strings.Split(digestHeader, "=") if len(parts) != 2 || strings.ToLower(parts[0]) != "sha-256" { return "", errors.New("invalid digest header format") } return parts[1], nil } // Compute hash from body h := sha256.New() h.Write(body) hashBytes := h.Sum(nil) return base64.StdEncoding.EncodeToString(hashBytes), nil } // VerifySignature verifies an RFC 9421 signature. // signedString is the string that was signed (e.g., "@method @target-uri @host @date digest"). // signature is the base64-encoded signature from the Signature header. // secret is the raw secret key. func VerifySignature(signedString, signature, secret string) error { h := hmac.New(sha256.New, []byte(secret)) h.Write([]byte(signedString)) expectedMAC := h.Sum(nil) actualSig, err := base64.StdEncoding.DecodeString(signature) if err != nil { return errors.New("invalid signature encoding") } if !hmac.Equal(expectedMAC, actualSig) { return errors.New("signature verification failed") } return nil } // ComputeSignedString constructs the string to sign from request components. // components should be in the order specified by the Signature-Input header. func ComputeSignedString(components map[string]string, order []string) string { var parts []string for _, key := range order { if value, ok := components[key]; ok { parts = append(parts, value) } } return strings.Join(parts, "\n") }