Skip to content

Commit f00f895

Browse files
committed
feat: add hmac signer
1 parent d4edc5d commit f00f895

File tree

11 files changed

+342
-0
lines changed

11 files changed

+342
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Signing\Exceptions;
4+
5+
interface SigningException
6+
{
7+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Signing\Exceptions;
4+
5+
use Exception;
6+
7+
final class SigningKeyWasMissing extends Exception implements SigningException
8+
{
9+
public function __construct()
10+
{
11+
parent::__construct('Signing key is not configured. Ensure you have a `SIGNING_KEY` environment variable.');
12+
}
13+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Signing;
4+
5+
use Tempest\Cryptography\Signing\Exceptions\SigningKeyWasMissing;
6+
7+
final class GenericSigner implements Signer
8+
{
9+
public SigningAlgorithm $algorithm {
10+
get => $this->config->algorithm;
11+
}
12+
13+
private string $key {
14+
get {
15+
if (trim($this->config->key) === '') {
16+
throw new SigningKeyWasMissing();
17+
}
18+
19+
return $this->config->key;
20+
}
21+
}
22+
23+
public function __construct(
24+
private readonly SigningConfig $config,
25+
) {}
26+
27+
public function sign(string $data): Signature
28+
{
29+
return new Signature(hash_hmac(
30+
algo: $this->algorithm->value,
31+
data: $data,
32+
key: $this->key,
33+
));
34+
}
35+
36+
public function verify(string $data, Signature $signature): bool
37+
{
38+
return hash_equals(
39+
known_string: $this->sign($data)->signature,
40+
user_string: $signature->signature,
41+
);
42+
}
43+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Signing;
4+
5+
use Stringable;
6+
7+
final readonly class Signature implements Stringable
8+
{
9+
public function __construct(
10+
public string $signature,
11+
) {}
12+
13+
public function __toString(): string
14+
{
15+
return $this->signature;
16+
}
17+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Signing;
4+
5+
interface Signer
6+
{
7+
public SigningAlgorithm $algorithm {
8+
get;
9+
}
10+
11+
/**
12+
* Signs the given data.
13+
*/
14+
public function sign(string $data): Signature;
15+
16+
/**
17+
* Verifies the integrity and provenance of the given data thanks to the given user-provided signature.
18+
*/
19+
public function verify(string $data, Signature $signature): bool;
20+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Signing;
4+
5+
use Tempest\Container\Container;
6+
use Tempest\Container\Initializer;
7+
use Tempest\Container\Singleton;
8+
9+
final class SignerInitializer implements Initializer
10+
{
11+
#[Singleton]
12+
public function initialize(Container $container): Signer
13+
{
14+
return new GenericSigner($container->get(SigningConfig::class));
15+
}
16+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Signing;
4+
5+
enum SigningAlgorithm: string
6+
{
7+
case SHA256 = 'sha256';
8+
case SHA512 = 'sha512';
9+
case SHA3_256 = 'sha3-256';
10+
case SHA3_512 = 'sha3-512';
11+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Signing;
4+
5+
final class SigningConfig
6+
{
7+
/**
8+
* @param SigningAlgorithm $algorithm The algorithm used for signing and verifying signatures.
9+
* @param non-empty-string $key The key used for signing and verifying signatures.
10+
*/
11+
public function __construct(
12+
public SigningAlgorithm $algorithm,
13+
#[\SensitiveParameter]
14+
public string $key,
15+
) {}
16+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
use Tempest\Cryptography\Signing\SigningAlgorithm;
4+
use Tempest\Cryptography\Signing\SigningConfig;
5+
6+
return new SigningConfig(
7+
algorithm: SigningAlgorithm::SHA256,
8+
key: Tempest\env('SIGNING_KEY', default: ''),
9+
);
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Tests\Signing;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Tempest\Cryptography\Signing\Exceptions\SigningKeyWasMissing;
7+
use Tempest\Cryptography\Signing\GenericSigner;
8+
use Tempest\Cryptography\Signing\SigningAlgorithm;
9+
use Tempest\Cryptography\Signing\SigningConfig;
10+
11+
final class SignerTest extends TestCase
12+
{
13+
public function test_good_signature(): void
14+
{
15+
$signer = new GenericSigner(new SigningConfig(
16+
algorithm: SigningAlgorithm::SHA256,
17+
key: 'my_secret_key',
18+
));
19+
20+
$data = 'important data';
21+
$signature = $signer->sign($data);
22+
23+
$this->assertTrue($signer->verify($data, $signature));
24+
}
25+
26+
public function test_bad_signature(): void
27+
{
28+
$signer = new GenericSigner(new SigningConfig(
29+
algorithm: SigningAlgorithm::SHA256,
30+
key: 'my_secret_key',
31+
));
32+
33+
$data = 'important data';
34+
$signature = $signer->sign($data);
35+
36+
// Tamper with the data
37+
$tamperedData = 'tampered data';
38+
39+
$this->assertFalse($signer->verify($tamperedData, $signature));
40+
}
41+
42+
public function test_different_algoritms(): void
43+
{
44+
$signer1 = new GenericSigner(new SigningConfig(
45+
algorithm: SigningAlgorithm::SHA256,
46+
key: 'my_secret_key',
47+
));
48+
49+
$signer2 = new GenericSigner(new SigningConfig(
50+
algorithm: SigningAlgorithm::SHA512,
51+
key: 'my_secret_key',
52+
));
53+
54+
$data = 'important data';
55+
$signature1 = $signer1->sign($data);
56+
$signature2 = $signer2->sign($data);
57+
58+
// Signatures should be different due to different algorithms
59+
$this->assertNotEquals($signature1, $signature2);
60+
61+
// Verify with the correct signer
62+
$this->assertTrue($signer1->verify($data, $signature1));
63+
$this->assertTrue($signer2->verify($data, $signature2));
64+
65+
// Verify with the wrong signer
66+
$this->assertFalse($signer1->verify($data, $signature2));
67+
$this->assertFalse($signer2->verify($data, $signature1));
68+
}
69+
70+
public function test_no_signing_key(): void
71+
{
72+
$this->expectException(SigningKeyWasMissing::class);
73+
74+
$signer = new GenericSigner(new SigningConfig(
75+
algorithm: SigningAlgorithm::SHA256,
76+
key: '',
77+
));
78+
79+
$signer->sign('important data');
80+
}
81+
82+
public function test_empty_data(): void
83+
{
84+
$signer = new GenericSigner(new SigningConfig(
85+
algorithm: SigningAlgorithm::SHA256,
86+
key: 'my_secret_key',
87+
));
88+
89+
$signature = $signer->sign('');
90+
91+
// An empty string should still produce a valid signature
92+
$this->assertTrue($signer->verify('', $signature));
93+
}
94+
95+
public function test_consistent_signature(): void
96+
{
97+
$signer = new GenericSigner(new SigningConfig(
98+
algorithm: SigningAlgorithm::SHA256,
99+
key: 'my_secret_key',
100+
));
101+
102+
$data = 'important data';
103+
$signature1 = $signer->sign($data);
104+
$signature2 = $signer->sign($data);
105+
106+
// Signing the same data should produce the same signature
107+
$this->assertEquals($signature1, $signature2);
108+
}
109+
110+
public function test_different_keys(): void
111+
{
112+
$signer1 = new GenericSigner(new SigningConfig(
113+
algorithm: SigningAlgorithm::SHA256,
114+
key: 'signer1_key_foo',
115+
));
116+
117+
$signer2 = new GenericSigner(new SigningConfig(
118+
algorithm: SigningAlgorithm::SHA512,
119+
key: 'signer2_key_bar',
120+
));
121+
122+
$data = 'important data';
123+
$signature1 = $signer1->sign($data);
124+
$signature2 = $signer2->sign($data);
125+
126+
// Signatures should be different due to different keys
127+
$this->assertNotEquals($signature1, $signature2);
128+
129+
// Verify with the correct signer
130+
$this->assertTrue($signer1->verify($data, $signature1));
131+
$this->assertTrue($signer2->verify($data, $signature2));
132+
133+
// Verify with the wrong signer
134+
$this->assertFalse($signer1->verify($data, $signature2));
135+
$this->assertFalse($signer2->verify($data, $signature1));
136+
}
137+
}

0 commit comments

Comments
 (0)