feat(certificates): read the identity a Brazilian signer is known by - #247
Merged
Conversation
This package exists for Brazilian signing and could not answer the first
question anyone asks of a signed document: who signed it. In Brazil that
is a CPF or a CNPJ, and the package handed back "JOAO DA SILVA:11144477735"
with the number glued to the name.
So every consumer wrote explode(':', $commonName), which breaks on a name
containing a colon and is simply wrong for an e-CNPJ: its common name
carries the company while the CPF in the extension belongs to whoever
answers for it.
The structured identity lives in subjectAlternativeName, as otherName
entries under 2.16.76.1.3, and openssl_x509_parse() renders every one of
them as `othername:<unsupported>`. The Asn1Reader 0019 built for the CMS
can go where it cannot.
Three details of the layout are each a way to read a field wrong, and are
handled rather than assumed: there are no separators, so a field read one
character short reads the next one wrong; the last field of a layout may
run short, which the specification states about the RG's issuing
authority; and "unavailable" is written as zeros, which comes back as null
because zeros and absence are the same fact.
IcpBrasilValidator checks what the specification states about the bytes:
required fields, widths, the A-Z 0-9 alphabet, modulus eleven on both
numbers, a real birth date, an issuer named for an RG that is absent, and
the two places a CPF appears agreeing.
conforms() is not isTrusted(), and keeping them apart is the whole risk
here. DebugCertificate::icpBrasil() builds a self-signed certificate that
satisfies every rule, which is what the tests run against.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This package exists for Brazilian signing and could not answer the first question anyone asks of a signed document: who signed it.
In Brazil that answer is a CPF or a CNPJ. The package handed back this:
So every consumer wrote
explode(':', $commonName). That breaks on a name containing a colon, and it is simply wrong for an e-CNPJ: its common name carries the company, while the CPF in the extension belongs to whoever answers for it.Now
Why it was not read before
openssl_x509_parse()cannot do it. Every ICP-Brasil field is anotherNameunder 2.16.76.1.3, and PHP renders each one asothername:<unsupported>. TheAsn1Readerthat 0019 built for the CMS goes where it cannot.Three ways to read the layout wrong, from the specification itself
Verified against the Receita Federal's certificate layout, §2.2.5 for e-CPF and §3.2.5 for e-CNPJ, not from memory:
The structural check
A1PdfSign::icpBrasil($pfxPath, $password)says which field is wrong, from the file, before anything is signed.otherNameentries for e-CPF, four for e-CNPJddmmyyyyconforms()is notisTrusted()A self-signed certificate can be built to satisfy every rule above, and this PR builds one:
DebugCertificate::icpBrasil()is what the tests run against. It chains to nothing and no trust store will accept it.The value is upstream of trust rather than instead of it. A certificate that fails here will be read wrong by everything downstream, and finding that out from the bytes beats finding it out from a rejected filing. Stated in the class docblock, the report docblock, the contract and 0029, because it is the one thing about this feature that is dangerous to get wrong.
Surface
Contracts\A1PdfSigngainedicpBrasil(): a break for implementers, which Roave reports.Data\Signergained$icpBrasilandname(), appended with defaults.Pkcs7Reader::signers()goes through the PEM rather than through a parse, because the identity is only in the bytes.Support\NationalRegistryis bespoke by necessity: neither Laravel nor any dependency here validates a CPF.It says a number is well formed, never that it exists. Whether the Receita Federal issued it is a question only they answer, and asking would mean a network call from validation, which nothing here does.
composer checkgreen: 473 passing, 30 of them new, PHPStan level max, Pint clean.