You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
toPem() checks certificate data against its label but not private-key data #626
toPem() checks certificate data but not private-key data. The data of a CERTIFICATE message goes to Node's X509Certificate, and the value is refused unless it holds exactly one certificate (formatPemMessage()). There is no matching check for keys. Given base64 and "PRIVATE KEY", toPem() writes that label around whatever the data is and returns it. If the data is a PKCS #1RSAPrivateKey, a public key, or not a key at all, the caller gets a PEM that Node can't read, with no error. The failure shows up later, inside Node's crypto, as DECODER routines::unsupported. pemToDer() likewise returns the bytes of such a message.
PKCS #1 is the case that matters in practice. Keys written as RSA PRIVATE KEY are common: OpenSSL 1.x's genrsa wrote them, and so do many keys already deployed. Strip the boundaries to store such a key on one line and nothing in the base64 says which structure it is. A caller that labels it PRIVATE KEY, which is RFC 7468's label for a private key, gets back a key that can't be used.
This came up in node-saml/node-saml#445, where the reporter's key is PKCS #1. node-saml/node-saml#447 works around it by parsing the key in node-saml. By the division of labour set out in #603, that puts the format check in the wrong library: xml-crypto owns "what a PEM message is, what base64 is, what a label may be", and node-saml owns "which label that option must carry".
Reproduction
Save as repro.js in the repo root and run npm run build && node repro.js. Tested at aa2cc0c.
The last toPem() row is the contrast: the same random bytes are refused when the label is CERTIFICATE.
Proposal
Check private-key data the way certificate data is already checked. In formatPemMessage() and pemToDer(), pass the data of a PRIVATE KEY or RSA PRIVATE KEY message to crypto.createPrivateKey() under that label, and throw Invalid PEM format. if Node can't read it.
Refuse rather than relabel. The label stays the caller's choice, as it already is for base64. A caller that accepts either structure, such as node-saml's privateKey, tries PRIVATE KEY and then RSA PRIVATE KEY, and this check tells it which one fits.
Use Node's reading, not a stricter structural one. OpenSSL reads PKCS question #8 data under RSA PRIVATE KEY (the fourth row above), and it does so even from DER typed pkcs1. So a check through Node accepts that mislabel. Refusing it would need an ASN.1 check of our own and would refuse input that works today. I'm not proposing that.
Scope to decide: the same reasoning covers EC PRIVATE KEY, PUBLIC KEY and RSA PUBLIC KEY, which Node also reads. The rule could be "every label Node can parse" rather than the two private-key labels. ENCRYPTED PRIVATE KEY keeps the base64 rules alone, since Node can't read it without the passphrase, as feat: own the PEM parser and validate the encapsulated text #603 notes.
A separate question: text around a message in toPem()
The README treats this as settled. pemCertificates() passes over explanatory text, "while toPem() rewrites the whole value and so must account for all of it." I'd like to revisit it for one case, though the answer may well still be no.
node-saml has always handed decryptionPvk to Node as given, and Node reads past text around the key, such as the Bag Attributes lines that openssl pkcs12 -nocerts writes. Routing decryptionPvk through toPem() would refuse keys that decrypt today, so node-saml/node-saml#447 has two paths: PEM goes to Node untouched, and only bare base64 goes through toPem(). As a result, a bad PEM decryptionPvk either fails inside Node's crypto without naming the option, or node-saml has to parse the key itself. If toPem() passed over text outside its messages, as pemCertificates() does, the second path would go away and decryptionPvk would be read exactly as privateKey is.
The cost is that toPem() would drop that text without saying so. The text carries no key material: RFC 7468 section 5.2 calls it explanatory, and section 2 permits data before the boundary. Still, the change loosens toPem() for every caller. That includes node-saml's privateKey and idpCert, whose documentation currently says such text is refused. If the answer is no, node-saml keeps its pass-through, and the proposal above goes ahead regardless.
Summary
toPem()checks certificate data but not private-key data. The data of aCERTIFICATEmessage goes to Node'sX509Certificate, and the value is refused unless it holds exactly one certificate (formatPemMessage()). There is no matching check for keys. Given base64 and"PRIVATE KEY",toPem()writes that label around whatever the data is and returns it. If the data is a PKCS #1RSAPrivateKey, a public key, or not a key at all, the caller gets a PEM that Node can't read, with no error. The failure shows up later, inside Node's crypto, asDECODER routines::unsupported.pemToDer()likewise returns the bytes of such a message.PKCS #1 is the case that matters in practice. Keys written as
RSA PRIVATE KEYare common: OpenSSL 1.x'sgenrsawrote them, and so do many keys already deployed. Strip the boundaries to store such a key on one line and nothing in the base64 says which structure it is. A caller that labels itPRIVATE KEY, which is RFC 7468's label for a private key, gets back a key that can't be used.This came up in node-saml/node-saml#445, where the reporter's key is PKCS #1. node-saml/node-saml#447 works around it by parsing the key in node-saml. By the division of labour set out in #603, that puts the format check in the wrong library: xml-crypto owns "what a PEM message is, what base64 is, what a label may be", and node-saml owns "which label that option must carry".
Reproduction
Save as
repro.jsin the repo root and runnpm run build && node repro.js. Tested at aa2cc0c.Output, identical on Node 18.20.8 and 26.9.0:
The last
toPem()row is the contrast: the same random bytes are refused when the label isCERTIFICATE.Proposal
Check private-key data the way certificate data is already checked. In
formatPemMessage()andpemToDer(), pass the data of aPRIVATE KEYorRSA PRIVATE KEYmessage tocrypto.createPrivateKey()under that label, and throwInvalid PEM format.if Node can't read it.toPem()returns today and Node then rejects, the same kind of case feat: own the PEM parser and validate the encapsulated text #603 started refusing in 6.3.0.privateKey, triesPRIVATE KEYand thenRSA PRIVATE KEY, and this check tells it which one fits.RSA PRIVATE KEY(the fourth row above), and it does so even from DER typedpkcs1. So a check through Node accepts that mislabel. Refusing it would need an ASN.1 check of our own and would refuse input that works today. I'm not proposing that.EC PRIVATE KEY,PUBLIC KEYandRSA PUBLIC KEY, which Node also reads. The rule could be "every label Node can parse" rather than the two private-key labels.ENCRYPTED PRIVATE KEYkeeps the base64 rules alone, since Node can't read it without the passphrase, as feat: own the PEM parser and validate the encapsulated text #603 notes.A separate question: text around a message in
toPem()The README treats this as settled.
pemCertificates()passes over explanatory text, "whiletoPem()rewrites the whole value and so must account for all of it." I'd like to revisit it for one case, though the answer may well still be no.node-saml has always handed
decryptionPvkto Node as given, and Node reads past text around the key, such as theBag Attributeslines thatopenssl pkcs12 -nocertswrites. RoutingdecryptionPvkthroughtoPem()would refuse keys that decrypt today, so node-saml/node-saml#447 has two paths: PEM goes to Node untouched, and only bare base64 goes throughtoPem(). As a result, a bad PEMdecryptionPvkeither fails inside Node's crypto without naming the option, or node-saml has to parse the key itself. IftoPem()passed over text outside its messages, aspemCertificates()does, the second path would go away anddecryptionPvkwould be read exactly asprivateKeyis.The cost is that
toPem()would drop that text without saying so. The text carries no key material: RFC 7468 section 5.2 calls it explanatory, and section 2 permits data before the boundary. Still, the change loosenstoPem()for every caller. That includes node-saml'sprivateKeyandidpCert, whose documentation currently says such text is refused. If the answer is no, node-saml keeps its pass-through, and the proposal above goes ahead regardless.