I'm working on a project that involves generating a signed, encrypted URL in PHP, which is then verified and decrypted in Go. However, I'm running into an issue with the HMAC signature verification step in Go not matching up with the signature generated in PHP, despite seemingly having mirrored the logic correctly.
PHP Code:
$signatureKey = 'd7057f70f4b0b5cd7b6f3272dcd53e68e7a9457a588e5be39abb69e9b76f27c1';
$signatureSalt = '839913fc2ab514aa985a02a4b4b693cc3b028a0e797bb83302eb96b2ca831661';
$signatureKeyBin = hex2bin($signatureKey);
$signatureSaltBin = hex2bin($signatureSalt);
function base64url_encode(string $data) {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
function encrypt(string $data, string $key) {
$tag = null;
$tagLength = 16;
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-gcm'));
$encrypted = openssl_encrypt($data, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, '', $tagLength);
return $iv.$encrypted.$tag;
}
$originalImageUrl = 'example/folder';
$encryptedBinaryUrl = encrypt($originalImageUrl, $signatureKeyBin);
$encryptedUrl = base64url_encode($encryptedBinaryUrl);
$encryptedPath = "/enc/{$encryptedUrl}";
$binarySignature = hash_hmac('sha256', $signatureSaltBin . $encryptedPath, $signatureKeyBin, true);
$signature = base64url_encode($binarySignature);
$signedUrl = sprintf("/%s%s", $signature, $encryptedPath);
echo 'http://localhost:8080'.$signedUrl;
Go Code (Simplified for Context):
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"os"
)
var key = []byte(os.Getenv("FILELIST_SECRET_KEY"))
var salt = []byte(os.Getenv("FILELIST_SECRET_SALT"))
func base64UrlDecode(data string) ([]byte, error) {
return base64.RawURLEncoding.DecodeString(data)
}
func verifySignature(signature, encryptedPath string) bool {
key := []byte(os.Getenv("FILELIST_SECRET_KEY"))
decodedSignature, err := base64UrlDecode(signature)
if err != nil {
fmt.Printf("Invalid signature decode error: %v\n", err)
return false
}
mac := hmac.New(sha256.New, key)
mac.Write(salt)
mac.Write([]byte(encryptedPath))
expectedMAC := mac.Sum(nil)
return hmac.Equal(decodedSignature, expectedMAC)
}
// `Signature` and `encryptedPath` are extracted from the request URL
func SignatureVerificationMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get the signature and encrypted parameters from the route or query parameters
vars := mux.Vars(r)
signature := vars["signature"]
encrypted := vars["encrypted"]
// Reassemble the URL path for signature verification
encPath := fmt.Sprintf("/%s/enc/%s", signature, encrypted)
// Decode signature
_, err := utils.Base64UrlDecode(signature)
if err != nil {
utils.OutputMessage(w, utils.HTTPResponse, http.StatusBadRequest, "Invalid signature format")
return
}
if !VerifySignature(signature, encPath) {
utils.OutputMessage(w, utils.HTTPResponse, http.StatusForbidden, "Invalid signature")
return
}
// Continue processing the rest of the request
next.ServeHTTP(w, r)
})
}
The issue arises when verifying the signature in Go—it doesn't match the one generated in PHP. I've checked the environment variables for the secret key on both sides and ensured they match. The method I use for base64 URL decoding in Go is as per Go’s encoding/base64
package.
Can anyone spot where the discrepancy might be between my Go signature verification and the PHP signature creation? Are there subtleties in the HMAC or base64 URL encoding/decoding process between PHP and Go that I’m missing?
Thank you in advance for your help!
So that the Go code is compatible with the PHP code:
import (
...
"encoding/hex"
)
var key, _ = hex.DecodeString("d7057f70f4b0b5cd7b6f3272dcd53e68e7a9457a588e5be39abb69e9b76f27c1") // hex decode key
var salt, _ = hex.DecodeString("839913fc2ab514aa985a02a4b4b693cc3b028a0e797bb83302eb96b2ca831661") // hex decode salt
...
// from PHP code: http://localhost:8080/HKgOQ6dFPxI9cMGCdSbpKStiLaGTlh5RHk2rQJ9PU6I/enc/lVo6cGCz5ML-pFr6CudX6MH0pyRJYaQ-PDGfxNRwDDSbHC7GDcaGyi-O
func main() {
signature := "HKgOQ6dFPxI9cMGCdSbpKStiLaGTlh5RHk2rQJ9PU6I"
encryptedUrl := "lVo6cGCz5ML-pFr6CudX6MH0pyRJYaQ-PDGfxNRwDDSbHC7GDcaGyi-O"
encryptedPath := fmt.Sprintf("/enc/%s", encryptedUrl) // remove signature from message to be verified
verified := verifySignature(signature, encryptedPath)
fmt.Println(verified) // true
}
With these changes, the ciphertext signed with the PHP code can be successfully verified with the Go code.