Add Line webhook signature verification helper
Implements LineSignature.IsValid() using HMAC-SHA256 + FixedTimeEquals to prevent timing attacks; includes xUnit tests for valid, tampered, and null/empty header cases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using ROLAC.API.Services.Notifications;
|
||||
using Xunit;
|
||||
|
||||
namespace ROLAC.API.Tests.Services.Notifications;
|
||||
|
||||
public class LineSignatureTests
|
||||
{
|
||||
private const string Secret = "test-channel-secret";
|
||||
|
||||
private static string Sign(string body)
|
||||
{
|
||||
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(Secret));
|
||||
return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(body)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValid_ReturnsTrue_ForMatchingSignature()
|
||||
{
|
||||
var body = """{"events":[]}""";
|
||||
var signature = Sign(body);
|
||||
|
||||
var result = LineSignature.IsValid(Secret, Encoding.UTF8.GetBytes(body), signature);
|
||||
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValid_ReturnsFalse_ForTamperedBody()
|
||||
{
|
||||
var signature = Sign("""{"events":[]}""");
|
||||
|
||||
var result = LineSignature.IsValid(Secret, Encoding.UTF8.GetBytes("""{"events":[1]}"""), signature);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValid_ReturnsFalse_ForNullOrEmptyHeader()
|
||||
{
|
||||
var body = Encoding.UTF8.GetBytes("""{"events":[]}""");
|
||||
|
||||
Assert.False(LineSignature.IsValid(Secret, body, null));
|
||||
Assert.False(LineSignature.IsValid(Secret, body, ""));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace ROLAC.API.Services.Notifications;
|
||||
|
||||
/// <summary>Verifies the X-Line-Signature header (HMAC-SHA256 of the raw body, base64).</summary>
|
||||
public static class LineSignature
|
||||
{
|
||||
public static bool IsValid(string channelSecret, byte[] rawBody, string? signatureHeader)
|
||||
{
|
||||
if (string.IsNullOrEmpty(signatureHeader)) return false;
|
||||
|
||||
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(channelSecret));
|
||||
var expected = Convert.ToBase64String(hmac.ComputeHash(rawBody));
|
||||
|
||||
return CryptographicOperations.FixedTimeEquals(
|
||||
Encoding.UTF8.GetBytes(expected),
|
||||
Encoding.UTF8.GetBytes(signatureHeader));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user