# k2gl/sshsig

> Sign and verify with the SSH signature format (SSHSIG) in PHP.

Use the same SSHSIG format as `ssh-keygen -Y` to sign and verify blobs from PHP.

## Install

```bash
composer require k2gl/sshsig
```

## Requirements

- PHP >=8.1

## Documentation

# k2gl/sshsig

[![CI](https://img.shields.io/github/actions/workflow/status/k2gl/sshsig/ci.yml?branch=main&label=CI&logo=github)](https://github.com/k2gl/sshsig/actions)
[![Latest Stable Version](https://img.shields.io/packagist/v/k2gl/sshsig)](https://packagist.org/packages/k2gl/sshsig)
[![Total Downloads](https://img.shields.io/packagist/dt/k2gl/sshsig)](https://packagist.org/packages/k2gl/sshsig)
[![PHPStan](https://img.shields.io/badge/PHPStan-level%209-2a5ea7)](https://phpstan.org/)
[![License](https://img.shields.io/packagist/l/k2gl/sshsig)](https://packagist.org/packages/k2gl/sshsig)

Sign and verify files with SSH keys in PHP, in the OpenSSH SSHSIG format that `ssh-keygen -Y
sign/verify` produces (the signatures Git uses for commit and tag signing). Verifying checks
the signer against an `allowed_signers` file; signing emits bytes `ssh-keygen -Y verify`
accepts. No dependencies.

Fail-closed by design: anything malformed, unsupported, cryptographically invalid, or
unauthorized throws — nothing is ever silently treated as verified.

## Install

```bash
composer require k2gl/sshsig
```

Requires PHP 8.1+ with `ext-sodium` (Ed25519) and `ext-openssl` (RSA and ECDSA) — both are
bundled with most PHP builds.

## Usage

### Verify a signature against an allowed_signers file

This is the equivalent of `ssh-keygen -Y verify -f allowed_signers -I <identity> -n <namespace>`.

```php
use K2gl\Sshsig\AllowedSigners;
use K2gl\Sshsig\Exception\SshsigException;
use K2gl\Sshsig\SshsigVerifier;

$verifier = new SshsigVerifier;

try {
    $result = $verifier->verify(
        message: file_get_contents('release.tar.gz'),
        armoredSignature: file_get_contents('release.tar.gz.sig'),
        allowedSigners: AllowedSigners::fromFile('allowed_signers'),
        identity: 'alice@example.com',
        namespace: 'file',
    );

    echo "Verified: {$result->publicKey->fingerprint()}\n";
} catch (SshsigException $e) {
    // not verified — malformed, unsupported, bad signature, or unauthorized signer
    echo "Rejected: {$e->getMessage()}\n";
}
```

### Check a signature without authorizing the signer

The equivalent of `ssh-keygen -Y check-novalidate -n <namespace>`: confirm the signature is
structurally sound and cryptographically valid under its own embedded key, then inspect it.

```php
$signature = $verifier->checkNoValidate($message, $armoredSignature, namespace: 'git');

echo $signature->signatureAlgorithm;          // e.g. "ssh-ed25519"
echo $signature->publicKey->fingerprint();    // "SHA256:…"
```

### Sign a message

Produce an SSHSIG signature (the `ssh-keygen -Y sign` operation). Provide the signing key —
Ed25519 (a 64-byte libsodium secret key) or an OpenSSL RSA/ECDSA private key:

```php
use K2gl\Sshsig\Ed25519SigningKey;
use K2gl\Sshsig\OpensshPrivateKey;
use K2gl\Sshsig\OpensslSigningKey;
use K2gl\Sshsig\SshsigSigner;

// Ed25519
$keypair = sodium_crypto_sign_keypair();
$signer = new SshsigSigner(new Ed25519SigningKey(sodium_crypto_sign_secretkey($keypair)));

// …or RSA / ECDSA from an OpenSSL private key (PEM)
$signer = new SshsigSigner(OpensslSigningKey::fromPem(file_get_contents('id_rsa')));

// …or an unencrypted ssh-ed25519 key straight off disk, in the format ssh-keygen writes
$signer = new SshsigSigner(OpensshPrivateKey::fromFile('/home/you/.ssh/id_ed25519'));

$armored = $signer->sign($message, namespace: 'file');          // sha512 by default
file_put_contents('message.sig', $armored);

// the resulting signature verifies with `ssh-keygen -Y verify` and with this library
```

`$signer->publicKey()` returns the matching `SshPublicKey` (e.g. to build an `allowed_signers`
line). The default hash is `sha512`; pass `hashAlgorithm: 'sha256'` to switch.

### Inspect or build allowed_signers in memory

```php
$allowed = AllowedSigners::fromString(<<<TXT
    # comments and blank lines are ignored
    alice@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA…
    *@example.com namespaces="git,file" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA…
    TXT);
```

## Design

- **Fail-closed.** Every failure path throws a `K2gl\Sshsig\Exception\SshsigException`
  (`InvalidSignatureException`, `UnsupportedAlgorithmException`, `SignatureVerificationFailed`,
  `SignerNotAllowedException`). A returned value always means verified.
- **Spec-accurate.** Implements OpenSSH `PROTOCOL.sshsig`: the `SSHSIG` magic preamble,
  version 1, the `to-be-signed` blob (namespace + reserved + hash algorithm + message hash),
  and the armor.
- **Algorithms.** `ssh-ed25519`, `rsa-sha2-256`, `rsa-sha2-512`,
  `ecdsa-sha2-nistp256/384/521`; `sha256` and `sha512` message hashing. The legacy SHA-1
  `ssh-rsa` signature algorithm is refused.
- **`allowed_signers`.** Principals pattern-lists, `namespaces=`, `valid-after`/`valid-before`
  validity windows, and `cert-authority` entries (parsed and recorded; certificate-chain
  verification is not yet performed).
- **Signing.** `SshsigSigner` over a pluggable `SigningKey` (Ed25519 via ext-sodium; RSA and
  ECDSA via ext-openssl), producing armor byte-compatible with `ssh-keygen -Y verify`.
  `OpensshPrivateKey::fromFile()`/`::fromString()` load an unencrypted `ssh-ed25519` key straight
  from the `openssh-key-v1` container `ssh-keygen` writes by default; encrypted containers and
  RSA/ECDSA containers are refused (fail-closed) rather than partially read.
- **Zero dependencies.** Pure PHP over `ext-sodium` and `ext-openssl`; no `phpseclib`.
- **Verified against OpenSSH.** The test suite verifies real `ssh-keygen -Y sign` output for
  every supported algorithm, plus tampered, wrong-namespace, unauthorized, and malformed cases.

## License

MIT. See [LICENSE](LICENSE).

## API

### K2gl\Sshsig\AllowedSigner (class)

- `__construct( public readonly string $principals, public readonly SshPublicKey $key, public readonly bool $certAuthority = false, public readonly ?string $namespaces = null, public readonly ?DateTimeImmutable $validAfter = null, public readonly ?DateTimeImmutable $validBefore = null, )`

### K2gl\Sshsig\AllowedSigners (class)

- `__construct(private readonly array $signers)`
- `fromString(string $contents): self`
- `fromFile(string $path): self`
- `all(): array`
- `findMatch(string $identity, string $namespace, SshPublicKey $key, DateTimeImmutable $time): ?AllowedSigner`

### K2gl\Sshsig\Ed25519SigningKey (class)

- `__construct(private readonly string $secretKey)`
- `publicKey(): SshPublicKey`
- `signTosign(string $tosign, string $hashAlgorithm): array`

### K2gl\Sshsig\Exception\InvalidSignatureException (class)

_no public methods_

### K2gl\Sshsig\Exception\SignatureVerificationFailed (class)

_no public methods_

### K2gl\Sshsig\Exception\SignerNotAllowedException (class)

_no public methods_

### K2gl\Sshsig\Exception\SigningException (class)

_no public methods_

### K2gl\Sshsig\Exception\SshsigException (interface)

_no public methods_

### K2gl\Sshsig\Exception\UnsupportedAlgorithmException (class)

_no public methods_

### K2gl\Sshsig\OpensshPrivateKey (class)

- `fromFile(string $path): SigningKey`
- `fromString(string $armored): SigningKey`

### K2gl\Sshsig\OpensslSigningKey (class)

- `fromKey(OpenSSLAsymmetricKey $key): self`
- `fromPem(string $pem, ?string $passphrase = null): self`
- `publicKey(): SshPublicKey`
- `signTosign(string $tosign, string $hashAlgorithm): array`

### K2gl\Sshsig\SigningKey (interface)

- `publicKey(): SshPublicKey`
- `signTosign(string $tosign, string $hashAlgorithm): array`

### K2gl\Sshsig\SshPublicKey (class)

- `fromBlob(string $blob): self`
- `fromOpensshLine(string $line): self`
- `fingerprint(): string`
- `equals(self $other): bool`

### K2gl\Sshsig\SshSignature (class)

- `fromArmored(string $armored): self`
- `signedData(string $message): string`

### K2gl\Sshsig\SshsigSigner (class)

- `__construct(private readonly SigningKey $key)`
- `publicKey(): SshPublicKey`
- `sign(string $message, string $namespace, string $hashAlgorithm = 'sha512'): string`

### K2gl\Sshsig\SshsigVerifier (class)

- `verify( string $message, string $armoredSignature, AllowedSigners $allowedSigners, string $identity, string $namespace, ?DateTimeImmutable $verifyTime = null, ): VerifiedSignature`
- `checkNoValidate(string $message, string $armoredSignature, string $namespace): SshSignature`

### K2gl\Sshsig\VerifiedSignature (class)

- `__construct( public readonly string $identity, public readonly string $namespace, public readonly SshPublicKey $publicKey, public readonly string $principals, )`

## Links

- GitHub: https://github.com/k2gl/sshsig
- Packagist: https://packagist.org/packages/k2gl/sshsig
