Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Key Utils Laravel Package

bitpay/key-utils

PHP utilities from BitPay for Bitcoin wallet key management, including BIP32 hierarchical deterministic key derivation and BIP39 mnemonic support, with helpers for generating, deriving, and handling secure keys in any PHP project.

View on GitHub
Deep Wiki
Context7

Getting Started

bitpay/key-utils is a small, framework-agnostic PHP cryptography package. It is not a Laravel package: there is no service provider, facade, config file, or migration. You install it, call static methods on BitPay\KeyUtils, and wire it into your app yourself.

Install and verify the environment

composer require bitpay/key-utils

The package depends on PHP's OpenSSL extension (and, depending on version, GMP). Before building anything, confirm your runtime can create the curve BitPay uses:

// tinker or a one-off route
dd(in_array('secp256k1', openssl_get_curve_names(), true));

If that returns false, stop and fix the platform before writing application code. On OpenSSL 3 this usually means the secp256k1 curve is unavailable through the default provider, or the container lacks the right OpenSSL build.

Where to look first

  • vendor/bitpay/key-utils/src/BitPay/KeyUtils.php — the whole public API lives here.
  • vendor/bitpay/key-utils/tests/ — the most reliable usage examples for the installed version.
  • composer show bitpay/key-utils — confirm the exact version and PHP constraint.

Do not assume the API is identical across versions. Read KeyUtils.php in the vendor directory and treat that as the contract.

First use case: a merchant key pair, SIN, and signature

The typical first task is generating a key pair for a BitPay merchant/store, deriving the public identity (SIN), and signing a request body.

use BitPay\KeyUtils;

$privateKey = KeyUtils::generateKey();                 // PEM-encoded EC private key
$publicKey  = KeyUtils::derivePublicKey($privateKey);  // PEM-encoded public key
$sin        = KeyUtils::getSin($publicKey);            // merchant identity

$payload   = json_encode(['facade' => 'merchant', 'label' => 'My Store']);
$signature = KeyUtils::sign($privateKey, $payload);    // base64 ECDSA signature

$valid = KeyUtils::verify($publicKey, $payload, $signature); // bool

Store the private key encrypted, store the public key and SIN alongside it, and never regenerate the pair per request. Key generation is relatively slow and, more importantly, rotating keys breaks every token and pairing associated with the old key.

Minimal config: none from the package. Put your own values in config/bitpay.php and read them through config(), not env(), so php artisan config:cache behaves correctly.

Implementation Patterns

Bind a thin wrapper in the container

Do not scatter KeyUtils:: calls through controllers and jobs. Create a small service that owns key loading and exposes intention-revealing methods.

namespace App\Services;

use BitPay\KeyUtils;

class BitPayKeyService
{
    public function __construct(private readonly string $privateKeyPem)
    {
    }

    public function sin(): string
    {
        return KeyUtils::getSin(
            KeyUtils::derivePublicKey($this->privateKeyPem)
        );
    }

    public function sign(string $payload): string
    {
        return KeyUtils::sign($this->privateKeyPem, $payload);
    }

    public function verify(string $payload, string $signature): bool
    {
        return KeyUtils::verify(
            KeyUtils::derivePublicKey($this->privateKeyPem),
            $payload,
            $signature
        );
    }
}

Resolve the PEM from a BitPayKey model or secret manager in a service provider:

$this->app->bind(BitPayKeyService::class, function () {
    $record = BitPayKey::where('active', true)->firstOrFail();

    return new BitPayKeyService(
        \Crypt::decryptString($record->private_key_encrypted)
    );
});

Cache the derived public key and SIN, not the private key. Deriving the public key on every request is unnecessary work; decrypting and holding the private key longer than needed is a security concern.

Persist keys properly

A simple model works well:

Schema::create('bitpay_keys', function (Blueprint $table) {
    $table->id();
    $table->text('private_key_encrypted'); // Crypt::encryptString($pem)
    $table->text('public_key');            // safe to store plaintext
    $table->string('sin')->unique();
    $table->string('label')->nullable();
    $table->boolean('active')->default(true);
    $table->timestamps();
});

Use Laravel's Crypt for at-rest encryption. The encrypted payload is base64 text, so a text column is appropriate. Keep the public key and SIN in plaintext; they are identities, not secrets.

Sign outgoing BitPay API requests

BitPay's JSON API expects the exact request body signed and sent in the x-signature header, with the SIN in x-identity. The critical detail is that you must sign the exact bytes you transmit. Encode the JSON once, sign that string, and send that same string.

With Laravel's HTTP client:

$body = json_encode($payload, JSON_UNESCAPED_SLASHES);

$response = Http::withBody($body, 'application/json')
    ->withHeaders([
        'x-identity'  => $keys->sin(),
        'x-signature' => $keys->sign($body),
        'x-accept-version' => '2.0.0',
    ])
    ->post('https://bitpay.com/invoices');

If you use Guzzle directly, a middleware keeps signing out of every call site:

use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;

$stack->push(Middleware::mapRequest(function (RequestInterface $request) use ($keys) {
    $body = (string) $request->getBody();

    return $request
        ->withHeader('x-identity', $keys->sin())
        ->withHeader('x-signature', $keys->sign($body));
}));

Avoid Http::post($url, $array) in signed flows, because you lose control over the exact serialized body. JSON key order, whitespace, and slash escaping all change the bytes and therefore the signature.

Verify incoming webhooks and callbacks

Anything claiming to come from BitPay should be verified before you trust it. Verify the raw request body, not the re-encoded array:

$raw = $request->getContent();
$signature = $request->header('x-signature');

abort_unless($keys->verify($raw, $signature), 401);

Use $request->getContent() rather than $request->all() or json_encode($request->all()). Middleware that parses and re-serializes JSON will break signature verification.

Pairing and token creation

The usual lifecycle is: generate a key pair, derive the SIN, create a pairing token with BitPay, then persist the returned token against the key. The key-utils package handles the cryptographic part only; the HTTP calls belong in your client service.

Keep a one-to-one or one-to-many relationship between keys and tokens, and record which key signed which token. When a token is revoked, mark the key inactive rather than deleting it, so audit history remains intact.

Testing

Do not generate fresh keys in every test. Generate one fixture pair once, commit it only if it is clearly test-only, and inject it into the service.

public function test_signature_round_trip(): void
{
    $private = file_get_contents(base_path('tests/fixtures/bitpay-test-key.pem'));
    $service = new BitPayKeyService($private);

    $payload = '{"facade":"merchant"}';
    $sig = $service->sign($payload);

    $this->assertTrue($service->verify($payload, $sig));
    $this->assertFalse($service->verify($payload . ' ', $sig));
}

For feature tests, bind a fake implementation of your own interface rather than hitting OpenSSL at all. The important behavior to test is "the request was signed with the right identity" and "invalid webhook signatures are rejected," not the internals of ECDSA.

Multi-store and key rotation

If you support multiple merchants, resolve the key by store/tenant rather than using one global key. A BitPayKeyRepository that accepts a store ID keeps this clean. For rotation, add a new active key, switch signing to it, keep the old key available for verifying incoming messages until BitPay stops using it, then deactivate it.

Gotchas and Tips

  • OpenSSL curve availability is the number one failure mode. Confirm secp256k1 is in openssl_get_curve_names(). On OpenSSL 3, missing legacy/provider configuration can cause key generation or derivation to fail with an opaque error. Always check openssl_error_string() when an operation returns false.
  • PEM newlines and .env do not mix. A PEM contains literal newlines. If you must put it in an environment variable, base64-encode it and decode at runtime, or store a file path. Escaped \n strings are a common source of "invalid key" errors.
  • Sign bytes, not data structures. The signature is valid only for the exact byte sequence. Do not sign json_encode($array), then send $request->all() or a client that re-encodes. Capture the body once and reuse it.
  • ECDSA signatures are non-deterministic. Two signatures over the same payload will differ. Never compare signature strings; always call verify.
  • Watch return types from OpenSSL. Verification may surface as bool, int (1/0/-1), or false on error. Normalize to a strict boolean at your service boundary so a -1 error never gets treated as success.
  • Do not log private keys or raw PEMs. Laravel's exception handler, query logging, and Telescope can all capture request/response payloads. Ensure the private key never enters a logged array. Use #[SensitiveParameter] on methods that accept the PEM if you are on PHP 8.2+.
  • Never cache the decrypted private key in a shared cache. Cache the public key and SIN; decrypt the private key per request or use a narrowly scoped singleton.
  • config:cache will bite you. Any env() call in package-adjacent code returns null after caching. Put values in config/bitpay.php and read them via config().
  • Package maturity is a real consideration. This is a low-star, low-dependent library. Pin the version in composer.json, read the installed source, and keep your own BitPayKeyService interface so you can swap to direct OpenSSL calls or a maintained SDK without touching call sites. The MIT license means forking or vendoring is viable if needed.
  • PHP version compatibility. Older releases may predate PHP 8 strictness or use deprecated extensions. Check composer show and run your test suite on the PHP version you deploy.
  • SIN derivation depends on the public key encoding. If you derive the SIN from a key in a different format than BitPay expects (for example, compressed vs. uncompressed public key handling), the resulting identity will not match and pairing will fail. Use the package's getSin() on the key produced by derivePublicKey() rather than computing it yourself.
  • Keep the public key and SIN as first-class stored values. They are safe to expose, useful for support and debugging, and let you identify which key signed a payload without decrypting anything.
  • Extension point: wrap, don't inherit. KeyUtils is a static utility, which is hard to mock. Your own interface with sign, verify, and sin methods gives you testability and a migration path. That wrapper is the single most valuable thing you can add around this package.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin