Search code examples
phpgoencryptionbase64hmac

Trouble with HMAC Signature Verification Between Go and PHP for Encrypted URL


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!


Solution

  • So that the Go code is compatible with the PHP code:

    • salt and key must be hex decoded,
    • and the signature must not be included in the message to be verified:
    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.