PHP Classes

File: src/HPKPBuilder.php

Recommend this page to a friend!
  Classes of Scott Arciszewski   PHP HPKP Builder   src/HPKPBuilder.php   Download  
File: src/HPKPBuilder.php
Role: Class source
Content type: text/plain
Description: Class source
Class: PHP HPKP Builder
Generate Public Key Pinning headers
Author: By
Last change:
Date: 4 years ago
Size: 6,413 bytes
 

Contents

Class file image Download
<?php
declare(strict_types=1);
namespace
ParagonIE\HPKPBuilder;

use
ParagonIE\ConstantTime\{
   
Base64,
   
Base64UrlSafe,
   
Binary,
   
Hex
};

/**
 * Class HPKPBuilder
 *
 * Quickly and easily build HTTP Public-Key-Pinning headers for your PHP
 * projects to mitigate the risk of MITM via rogue certificate authorities.
 *
 * @package ParagonIE\HPKPBuilder
 */
class HPKPBuilder
{
   
/**
     * @var string
     */
   
protected $compiled = '';

   
/**
     * @var array
     */
   
protected $config = [];

   
/**
     * @var bool
     */
   
protected $needsCompile = true;

   
/**
     * HPKPBuilder constructor
     *.
     * @param array $preloaded
     */
   
public function __construct(array $preloaded = [])
    {
        if (!empty(
$preloaded)) {
           
$this->config = $preloaded;
        }
    }

   
/**
     * Add a hash directly.
     *
     * @param string $hash
     * @param string $algo
     * @return self
     */
   
public function addHash(string $hash, string $algo = 'sha256'): self
   
{
        if (empty(
$this->config['hashes'])) {
           
$this->config['hashes'] = [];
        }
       
$hash = $this->coerceBase64($hash, $algo);
       
$this->config['hashes'][] = [
           
'algo' => $algo,
           
'hash' => $hash
       
];
       
$this->needsCompile = true;
        return
$this;
    }

   
/**
     * Compile the CSP header, store it in the protected $compiled property.
     *
     * @return self
     */
   
public function compile(): self
   
{
       
$includeSubs = $this->config['include-subdomains'] ?? false;
       
$hashes = $this->config['hashes'] ?? [];
       
$maxAge = $this->config['max-age'] ?? 5184000;
       
$reportOnly = $this->config['report-only'] ?? false;
       
$reportUri = $this->config['report-uri'] ?? null;
        if (empty(
$hashes)) {
           
// Send nothing.
           
$this->compiled = '';
            return
$this;
        }

       
$header = ($reportOnly && !empty($reportUri))
            ?
'Public-Key-Pins-Report-Only: '
           
: 'Public-Key-Pins: ';

        foreach (
$hashes as $h) {
           
$header .= 'pin-' . $h['algo'] . '=';
           
$header .= \json_encode($h['hash']);
           
$header .= '; ';
        }
       
$header .= 'max-age=' . $maxAge;

        if (
$includeSubs) {
           
$header .= '; includeSubDomains';
        }
        if (
$reportUri) {
           
$header .= '; report-uri="' . $reportUri . '"';
        }

       
$this->compiled = $header;
       
$this->needsCompile = false;
        return
$this;
    }

   
/**
     * Load configuration from a JSON file.
     *
     * @param string $filename
     * @return self
     * @throws \Exception
     */
   
public static function fromFile(string $filename = ''): self
   
{
        if (!
file_exists($filename)) {
            throw new \
Exception($filename.' does not exist');
        }
       
$json = \file_get_contents($filename);
        if (!\
is_string($json)) {
            throw new \
Exception('Could not read file.');
        }
       
$array = \json_decode($json, true);
        return new
HPKPBuilder($array);
    }

   
/**
     * @return string
     */
   
public function getHeader(): string
   
{
        if (
$this->needsCompile) {
           
$this->compile();
        }
        return
$this->compiled;
    }

   
/**
     * @return string
     */
   
public function getJSON(): string
   
{
        return \
json_encode($this->config);
    }

   
/**
     * Add the includeSubdomains directive in the HPKP header?
     *
     * @param bool $includeSubs
     * @return self
     */
   
public function includeSubdomains(bool $includeSubs = false): self
   
{
       
$this->config['include-subdomains'] = $includeSubs;
       
$this->needsCompile = true;
        return
$this;
    }

   
/**
     * Set the max-age parameter of the HPKP header
     *
     * @param int $maxAge
     * @return self
     */
   
public function maxAge(int $maxAge = 5184000): self
   
{
       
$this->config['max-age'] = $maxAge;
       
$this->needsCompile = true;
        return
$this;
    }

   
/**
     * Send a Report-Only header?
     *
     * @param bool $reportOnly
     * @return self
     */
   
public function reportOnly(bool $reportOnly = false): self
   
{
       
$this->config['report-only'] = $reportOnly;
       
$this->needsCompile = true;
        return
$this;
    }

   
/**
     * Set the report-uri parameter of the HPKP header
     *
     * @param string $reportURI
     * @return self
     */
   
public function reportUri(string $reportURI): self
   
{
       
$this->config['report-uri'] = $reportURI;
       
$this->needsCompile = true;
        return
$this;
    }

   
/**
     * Send the HPKP header
     *
     * @return bool
     */
   
public function sendHPKPHeader(): bool
   
{
        if (\
headers_sent()) {
            return
false;
        }
        \
header($this->getHeader());
        return
true;
    }

   
/**
     * Coerce a string into base64 format.
     *
     * @param string $hash
     * @param string $algo
     * @return string
     * @throws \Exception
     */
   
protected function coerceBase64(string $hash, string $algo = 'sha256'): string
   
{
        switch (
$algo) {
            case
'sha256':
               
$limits = [
                   
'raw' => 32,
                   
'hex' => 64,
                   
'pad_min' => 40,
                   
'pad_max' => 44
               
];
                break;
            default:
                throw new \
Exception(
                   
'Browsers currently only support sha256 public key pins.'
               
);
        }

       
$len = Binary::safeStrlen($hash);
        if (
$len === $limits['hex']) {
           
$hash = Base64::encode(Hex::decode($hash));
        } elseif (
$len === $limits['raw']) {
           
$hash = Base64::encode($hash);
        } elseif (
$len > $limits['pad_min'] && $len < $limits['pad_max']) {
           
// Padding was stripped!
           
$hash .= \str_repeat('=', $len % 4);

           
// Base64UrlSsafe encoded.
           
if (\strpos($hash, '_') !== false || \strpos($hash, '-') !== false) {
               
$hash = (string) Base64UrlSafe::decode((string) $hash);
            } else {
               
$hash = (string) Base64::decode((string) $hash);
            }
           
$hash = (string) Base64::encode((string) $hash);
        }
        return
$hash;
    }
}