Migrating from the sop libraries¶
PKI Framework started as a fork of the libraries published at
github.com/sop by Joni Eskelinen — sop/asn1, sop/x509, sop/x501,
sop/crypto-types, sop/crypto-encoding, sop/crypto-bridge. It has diverged substantially since, and
the two are no longer interchangeable. All credit for the original work goes to its developer.
This page is for code that used the originals.
One package instead of six¶
composer remove sop/asn1 sop/x509 sop/x501 sop/crypto-types sop/crypto-encoding sop/crypto-bridge
composer require spomky-labs/pki-framework
Everything is in one package now, autoloaded under one namespace.
The namespace¶
| Was | Is |
|---|---|
Sop\ASN1\… |
SpomkyLabs\Pki\ASN1\… |
Sop\X509\… |
SpomkyLabs\Pki\X509\… |
Sop\X501\… |
SpomkyLabs\Pki\X501\… |
Sop\CryptoTypes\… |
SpomkyLabs\Pki\CryptoTypes\… |
Sop\CryptoEncoding\… |
SpomkyLabs\Pki\CryptoEncoding\… |
Sop\CryptoBridge\… |
SpomkyLabs\Pki\CryptoBridge\… |
The class names and the directory layout under each are unchanged, so a search and replace over your
use statements covers most of the work:
grep -rl 'Sop\\' src tests | xargs sed -i \
-e 's/Sop\\ASN1\\/SpomkyLabs\\Pki\\ASN1\\/g' \
-e 's/Sop\\X509\\/SpomkyLabs\\Pki\\X509\\/g' \
-e 's/Sop\\X501\\/SpomkyLabs\\Pki\\X501\\/g' \
-e 's/Sop\\CryptoTypes\\/SpomkyLabs\\Pki\\CryptoTypes\\/g' \
-e 's/Sop\\CryptoEncoding\\/SpomkyLabs\\Pki\\CryptoEncoding\\/g' \
-e 's/Sop\\CryptoBridge\\/SpomkyLabs\\Pki\\CryptoBridge\\/g'
Check the diff: a string mentioning Sop\ in a message or a fixture is not a class reference.
new becomes create()¶
This is the change that touches the most lines. Constructors are private or protected, so every class
is built through a static factory and new no longer compiles.
// was
$seq = new Sequence(new Integer(1), new UTF8String('x'));
$name = new Name(...$rdns);
$validity = new Validity($notBefore, $notAfter);
$ext = new BasicConstraintsExtension(true, true, 1);
// is
$seq = Sequence::create(Integer::create(1), UTF8String::create('x'));
$name = Name::create(...$rdns);
$validity = Validity::create($notBefore, $notAfter);
$ext = BasicConstraintsExtension::create(true, true, 1);
The rule is mechanical: new X(…) becomes X::create(…), with the same arguments. The named factories
you already used — fromPEM(), fromDER(), fromASN1(), fromString() — are unchanged.
new OpenSSLCrypto() still works, since the crypto engine is meant to be extended. Step 2 of the order
below is the cheap way to find the rest: a missed new is a hard failure, not a silent one.
Immutability¶
Objects do not change. Every with*() returns a copy, and the original is untouched:
$tbs->withRandomSerialNumber(); // discarded
$tbs = $tbs->withRandomSerialNumber(); // what you meant
That was true of the originals in most places and is now true everywhere, including Extensions,
AttributeCollection and the ASN.1 structures.
DateTimeImmutable¶
Dates are DateTimeImmutable, never DateTime:
$validity->notBefore()->dateTime(); // DateTimeImmutable
PathValidationConfig::create(new DateTimeImmutable(), 3);
Large integers¶
Serial numbers, RSA moduli and ASN.1 integers come back as decimal strings, with brick/math underneath:
$tbs->serialNumber(); // "2569673129085639595562975411803018896"
$integer->number(); // decimal string
$integer->intNumber(); // int, throws if it does not fit
$integer->getValue(); // BigInteger
Comparing serial numbers as PHP integers silently stops working above PHP_INT_MAX. Compare the
strings, or use BigInteger.
Behaviour that changed, and will break code¶
These are the ones a search and replace will not find. Each is deliberate.
Path validation needs a trust anchor¶
// was: validated against the path's own first certificate, silently
$path->validate($config);
// is
$path->validate($config->withTrustAnchor($root));
// or, better, build the path from your trust list
$path = CertificationPath::toTarget($target, $trustAnchors, $intermediates);
$path->validate($config);
A path from fromCertificateChain() now throws without an anchor. A path from create() raises a
deprecation notice and still falls back, so you can find your call sites before the fallback goes. See
Validating a path.
fromCSR() no longer copies every requested extension¶
// was: every requested extension was copied
$tbs = TBSCertificate::fromCSR($csr);
// is: name what you honour
$tbs = TBSCertificate::fromCSR($csr, [Extension::OID_SUBJECT_ALT_NAME]);
The extensions that decide what a certificate may do are never copied, whatever you pass. Leaving the argument out raises a deprecation notice and will become an empty allow list. See Issuing from a request.
Certificate::equals() is now an exact comparison¶
It compares the two encodings octet by octet. It used to compare the serial number, the subject key identifier and the subject DN — three public values anyone can repeat — so it reported two certificates as the same when they differed in issuer, validity, every extension and the signature.
CertificateBundle::contains() is built on it and became exact with it. If you relied on the looser
grouping, hasEqualSubjectIdentity() is it — and read
what it does not tell you.
Signature algorithms and key sizes are filtered¶
Path validation refuses an algorithm outside allowedSignatureAlgorithms() and an RSA modulus below
minimumRSAKeySize(). MD2, MD4 and MD5 are refused by default; the defaults still accept SHA-1 and 1024
bits. A chain that used to validate may now be refused — which is the point. See
Configuration.
RSASSA-PSS certificates are refused when parsed¶
Not implemented, and deliberately absent from the allowed set rather than advertised and broken.
Decoding is bounded and its exceptions are normalised¶
Nesting is capped at 64 levels, and nothing but DecodeException escapes Element::fromDER() — the
original type is kept as its previous. A catch for something narrower may stop matching. See
Decoding untrusted input.
Serial numbers are random by default¶
sign() draws a 20-octet serial number when none was set, rather than writing a zero. A fixed serial
number below nine octets raises a deprecation notice.
Repeated optional fields are refused¶
A structure carrying the same context-specific tag twice raises DecodeException rather than being read
as its last copy.
Name comparison prepares each side under its own syntax¶
The same name written as a BMPString and as a PrintableString now compares equal, where it used to be
two different names — which meant a directoryName a constraint excluded was reachable by re-encoding
it. See Comparing names.
PHP version¶
PHP 8.1 or later, and mbstring is required. See
Prerequisites.
A migration order that works¶
- Replace the packages, run the namespace replacement, and let the autoloader find what is left.
- Replace
new X(…)withX::create(…), guided by the errors. Private constructors make every missed one a hard failure rather than a silent one. - Run your test suite with
E_USER_DEPRECATEDvisible. The two notices that matter — the path validation fallback and thefromCSR()deny list — are both security decisions, and both are going away. - Fix the
with*()return values you were discarding. - Re-read Security recommendations. Several defaults are stricter than the originals', and a few are still looser than a new deployment should accept.