From 3be217e811f0aec7d64465839e9147431e25c2d5 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 2 Aug 2026 16:09:14 +0200 Subject: [PATCH 01/22] test: update tests to run with OpenSSL >= 3.0 FIPS mode Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/64960 Fixes: https://github.com/nodejs/node/issues/48379 Reviewed-By: Antoine du Hamel Reviewed-By: Aviv Keller --- lib/internal/crypto/webcrypto.js | 1 + test/common/crypto.js | 9 +- test/fixtures/keys/Makefile | 28 ++ test/fixtures/keys/agent1-fips.pfx | Bin 0 -> 3808 bytes test/fixtures/keys/ec-fips.pfx | Bin 0 -> 1261 bytes test/parallel/test-crypto-argon2-job.js | 4 +- test/parallel/test-crypto-argon2.js | 15 +- .../parallel/test-crypto-async-sign-verify.js | 63 +++- .../test-crypto-authenticated-stream.js | 11 + test/parallel/test-crypto-authenticated.js | 32 +- test/parallel/test-crypto-certificate.js | 10 +- .../test-crypto-cipheriv-decipheriv.js | 31 +- test/parallel/test-crypto-classes.js | 15 +- test/parallel/test-crypto-dh-constructor.js | 7 +- test/parallel/test-crypto-dh-curves.js | 196 +++++------ test/parallel/test-crypto-dh-generate-keys.js | 5 +- test/parallel/test-crypto-dh-leak.js | 5 +- test/parallel/test-crypto-dh-modp2-views.js | 34 +- test/parallel/test-crypto-dh-modp2.js | 53 +-- test/parallel/test-crypto-dh-odd-key.js | 15 +- test/parallel/test-crypto-dh-shared.js | 6 +- test/parallel/test-crypto-dh.js | 8 +- test/parallel/test-crypto-ecdh-convert-key.js | 17 +- test/parallel/test-crypto-eddsa-variants.js | 14 +- test/parallel/test-crypto-encap-decap.js | 13 +- test/parallel/test-crypto-getcipherinfo.js | 9 +- test/parallel/test-crypto-hkdf.js | 8 +- test/parallel/test-crypto-hmac.js | 30 +- .../test-crypto-key-objects-messageport.js | 3 +- .../test-crypto-key-objects-to-crypto-key.js | 22 +- test/parallel/test-crypto-key-objects.js | 306 ++++++++++++------ test/parallel/test-crypto-key-store.js | 45 +-- test/parallel/test-crypto-keygen-async-dsa.js | 12 +- ...ypto-keygen-async-elliptic-curve-jwk-ec.js | 8 +- ...pto-keygen-async-elliptic-curve-jwk-rsa.js | 3 +- ...-crypto-keygen-async-elliptic-curve-jwk.js | 9 +- ...-keygen-async-encrypted-private-key-der.js | 6 +- ...ypto-keygen-async-encrypted-private-key.js | 11 +- ...nc-explicit-elliptic-curve-encrypted.js.js | 12 +- ...en-async-named-elliptic-curve-encrypted.js | 12 +- test/parallel/test-crypto-keygen-async-rsa.js | 15 +- .../parallel/test-crypto-keygen-bit-length.js | 27 +- .../test-crypto-keygen-deprecation.js | 8 +- .../parallel/test-crypto-keygen-dh-classic.js | 3 +- ...ypto-keygen-duplicate-deprecated-option.js | 8 +- test/parallel/test-crypto-keygen-eddsa.js | 9 +- ...crypto-keygen-empty-passphrase-no-error.js | 12 +- ...rypto-keygen-empty-passphrase-no-prompt.js | 23 +- ...o-keygen-invalid-parameter-encoding-dsa.js | 11 +- ...ypto-keygen-key-object-without-encoding.js | 5 +- .../test-crypto-keygen-key-objects.js | 8 +- .../test-crypto-keygen-missing-oid.js | 18 +- ...test-crypto-keygen-no-rsassa-pss-params.js | 9 +- ...pto-keygen-non-standard-public-exponent.js | 47 +-- test/parallel/test-crypto-keygen-promisify.js | 8 +- test/parallel/test-crypto-keygen-raw.js | 33 +- .../test-crypto-keygen-rfc8017-9-1.js | 7 +- .../test-crypto-keygen-rfc8017-a-2-3.js | 10 +- test/parallel/test-crypto-keygen-rsa-pss.js | 8 +- test/parallel/test-crypto-keygen-sync.js | 10 +- .../test-crypto-keyobject-brand-check.js | 5 +- .../test-crypto-keyobject-clone-transfer.js | 3 +- .../test-crypto-keyobject-hidden-slots.js | 16 +- .../test-crypto-keyobject-no-own-symbols.js | 3 +- test/parallel/test-crypto-pbkdf2.js | 185 ++++++++--- .../test-crypto-pqc-encrypted-pkcs8.js | 25 +- .../test-crypto-private-decrypt-gh32240.js | 42 ++- ...t-crypto-publicDecrypt-fails-first-time.js | 10 +- test/parallel/test-crypto-rsa-dsa.js | 146 ++++++--- test/parallel/test-crypto-scrypt.js | 47 ++- test/parallel/test-crypto-secure-heap.js | 5 +- test/parallel/test-crypto-sign-verify.js | 217 +++++++++---- test/parallel/test-crypto-worker-thread.js | 3 +- test/parallel/test-crypto.js | 65 ++-- .../test-https-agent-additional-options.js | 15 +- ...test-https-agent-pfx-object-array-reuse.js | 55 +++- .../test-https-agent-session-eviction.js | 31 +- test/parallel/test-https-pfx.js | 21 +- ...ttps-selfsigned-no-keycertsign-no-crash.js | 3 +- test/parallel/test-tls-alert.js | 21 +- .../test-tls-client-getephemeralkeyinfo.js | 82 +++-- test/parallel/test-tls-client-mindhsize.js | 15 +- test/parallel/test-tls-dhe.js | 55 +++- test/parallel/test-tls-ecdh-multiple.js | 27 +- .../test-tls-env-extra-ca-with-options.js | 25 +- test/parallel/test-tls-getprotocol.js | 42 ++- test/parallel/test-tls-honorcipherorder.js | 3 +- test/parallel/test-tls-invalid-pfx.js | 7 +- test/parallel/test-tls-min-max-version.js | 45 ++- test/parallel/test-tls-multi-key.js | 12 + test/parallel/test-tls-multi-pfx.js | 39 ++- test/parallel/test-tls-passphrase.js | 28 ++ .../test-tls-pfx-authorizationerror.js | 30 +- test/parallel/test-tls-session-cache.js | 9 +- test/parallel/test-tls-set-ciphers.js | 136 ++++---- test/parallel/test-tls-write-error.js | 19 +- ...-webcrypto-aead-decrypt-detached-buffer.js | 38 ++- test/parallel/test-webcrypto-constructors.js | 44 ++- .../test-webcrypto-cryptokey-hidden-slots.js | 3 +- .../test-webcrypto-deduplicate-usages.js | 21 +- .../test-webcrypto-derivebits-argon2.js | 4 +- .../test-webcrypto-derivebits-cfrg.js | 11 + test/parallel/test-webcrypto-derivebits.js | 61 +++- .../parallel/test-webcrypto-derivekey-cfrg.js | 11 + test/parallel/test-webcrypto-derivekey.js | 124 ++++--- test/parallel/test-webcrypto-digest.js | 40 ++- .../test-webcrypto-encrypt-decrypt-aes.js | 9 + ...rypto-encrypt-decrypt-chacha20-poly1305.js | 11 + .../test-webcrypto-encrypt-decrypt.js | 11 +- .../test-webcrypto-export-import-cfrg.js | 16 +- test/parallel/test-webcrypto-export-import.js | 9 +- .../test-webcrypto-get-public-key.mjs | 11 + test/parallel/test-webcrypto-keygen.js | 78 ++++- ...-webcrypto-promise-prototype-pollution.mjs | 61 +++- .../test-webcrypto-raw-format-aliases.js | 20 +- .../test-webcrypto-sign-verify-ecdsa.js | 33 +- .../test-webcrypto-sign-verify-eddsa.js | 4 +- .../test-webcrypto-sign-verify-hmac.js | 5 +- .../test-webcrypto-sign-verify-kmac.js | 92 ++++-- .../test-webcrypto-sign-verify-ml-dsa.js | 4 +- .../test-webcrypto-sign-verify-rsa.js | 44 ++- test/parallel/test-webcrypto-sign-verify.js | 3 +- test/parallel/test-webcrypto-supports.mjs | 5 + test/parallel/test-webcrypto-wrap-unwrap.js | 63 +++- ...t-crypto-argon2-nonblocking-constructor.js | 4 +- test/pummel/test-crypto-dh-keys.js | 12 +- test/pummel/test-dh-regr.js | 27 +- .../test-webcrypto-derivebits-pbkdf2.js | 26 +- test/sequential/test-async-wrap-getasyncid.js | 4 +- test/wpt/status/WebCryptoAPI.cjs | 86 ++++- 130 files changed, 2836 insertions(+), 1018 deletions(-) create mode 100644 test/fixtures/keys/agent1-fips.pfx create mode 100644 test/fixtures/keys/ec-fips.pfx diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index f53e93497c8b..7c62bab4ec8c 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -1674,6 +1674,7 @@ class SubtleCrypto { } // Implements https://wicg.github.io/webcrypto-modern-algos/#SubtleCrypto-method-supports + // TODO(panva): Make supports() account for the active FIPS state. static supports(operation, algorithm, lengthOrAdditionalAlgorithm = null) { emitExperimentalWarning('The supports Web Crypto API method'); if (this !== SubtleCrypto) throw new ERR_INVALID_THIS('SubtleCrypto constructor'); diff --git a/test/common/crypto.js b/test/common/crypto.js index 04b53acebdae..63f7487f15d3 100644 --- a/test/common/crypto.js +++ b/test/common/crypto.js @@ -50,9 +50,14 @@ function assertApproximateSize(key, expectedSize) { function testEncryptDecrypt(publicKey, privateKey) { const message = 'Hello Node.js world!'; const plaintext = Buffer.from(message, 'utf8'); + const withOaepHash = (key) => { + if (!hasFIPS(3)) return key; + if (key?.key !== undefined) return { ...key, oaepHash: 'sha256' }; + return { key, oaepHash: 'sha256' }; + }; for (const key of [publicKey, privateKey]) { - const ciphertext = publicEncrypt(key, plaintext); - const received = privateDecrypt(privateKey, ciphertext); + const ciphertext = publicEncrypt(withOaepHash(key), plaintext); + const received = privateDecrypt(withOaepHash(privateKey), ciphertext); assert.strictEqual(received.toString('utf8'), message); } } diff --git a/test/fixtures/keys/Makefile b/test/fixtures/keys/Makefile index 628e9f62c598..0960e69f36fd 100644 --- a/test/fixtures/keys/Makefile +++ b/test/fixtures/keys/Makefile @@ -8,6 +8,7 @@ all: \ ca5-cert.pem \ ca6-cert.pem \ agent1-cert.pem \ + agent1-fips.pfx \ agent1.pfx \ agent2-cert.pem \ agent3-cert.pem \ @@ -39,6 +40,7 @@ all: \ dsa_private_encrypted_1025.pem \ dsa_public_1025.pem \ ec-cert.pem \ + ec-fips.pfx \ ec.pfx \ fake-cnnic-root-cert.pem \ intermediate-ca-cert.pem \ @@ -445,6 +447,20 @@ agent1.pfx: agent1-cert.pem agent1-key.pem ca1-cert.pem -out agent1.pfx \ -password pass:sample +# PKCS12KDF is unavailable under FIPS properties. Use PBMAC1 with PBKDF2 +# instead, alongside AES-256/PBKDF2 key protection. +agent1-fips.pfx: agent1-cert.pem agent1-key.pem ca1-cert.pem + openssl pkcs12 -export \ + -keypbe AES-256-CBC \ + -certpbe AES-256-CBC \ + -iter 2048 \ + -pbmac1_pbkdf2 \ + -in agent1-cert.pem \ + -inkey agent1-key.pem \ + -certfile ca1-cert.pem \ + -out agent1-fips.pfx \ + -password pass:password + agent1-verify: agent1-cert.pem ca1-cert.pem openssl verify -CAfile ca1-cert.pem agent1-cert.pem @@ -788,6 +804,18 @@ ec.pfx: ec-cert.pem ec-key.pem -out ec.pfx \ -password pass: +# See agent1-fips.pfx for why the FIPS fixture uses PBMAC1. +ec-fips.pfx: ec-cert.pem ec-key.pem + openssl pkcs12 -export \ + -keypbe AES-256-CBC \ + -certpbe AES-256-CBC \ + -iter 2048 \ + -pbmac1_pbkdf2 \ + -in ec-cert.pem \ + -inkey ec-key.pem \ + -out ec-fips.pfx \ + -password pass:password + dh512.pem: openssl dhparam -out dh512.pem 512 diff --git a/test/fixtures/keys/agent1-fips.pfx b/test/fixtures/keys/agent1-fips.pfx new file mode 100644 index 0000000000000000000000000000000000000000..5613cfae430b4c83b74d8c7c0aac3fa4d1449d7f GIT binary patch literal 3808 zcmai%XHXOBwuTc(=tKx*(*@~06qT-YY0^6wy3(6Sl@fXak>0C-2#BGGAPUl}Kq%6Y zD!m7!8MvOgclMrp=FFTQ->iAxcg?r{t$7v<0huQN62cG=RU%S>m`5>Zlt5x&5dv}# zgn)?uu|;4AlAM1cq9Ozd>W>vm00jIg;r}E+82oPmri2;5#Qu>KFfI@kyfDIJK%_oS zsnm6}r+WiSKtKY3Q4o>*?^FU(FaX9%L~0ZB2~zZENUjcT(wNg2ggO-*mp0S-n15$N-dFw2Po zrjmI+mdXR_eZ{cwQ!xflkQurX{myXG3EB(q=bL%BO#j(SpW9#gzGwHavV_pQLMZIg zrL(Dv)YUazv|7%06^@{bof6D&d(}W@HR0MwvuVD){h+BAwG{53^}QuD#WrJ5TZ0T` z%VIvxj#06w-6%9kAnN{HKLO}qtX#SkCd#HL_!m&rcq$Pq#9Y?X5RmLveQMGV0>2V% zU!h<{dy+)cb6#}raJjf=cx~D)oNsGLY?$Z9S``J}nv=vX<|h*~%r`ZMg&L(j4GXy% zk#X;dQtZkhNMjSDJ14w@v_5C6>2vK553F6RY^b7dFW~-+cwsU_pNsplV6f|>T`a$h zUPH2o_V?VZg+7R>wjJhFh#h{?yhJRQF}MDr{zPN^lqVM(Xt+mLuuLYwYxYRe4zs@$ zq;Hv8ZkO+y+p1~vl9Zss%kL_B^+ND{n;C&A+L1E3Hb*e!FJU3@tRGO#k9h z{5f0?4#gZ`$W!x%JFH?=#96dT=|pw|WFW%)X1Y(sVgODLq3$ig)@_y5=l1k!b8rQM zaiiLZBzDgu;U9{Mofa#S;;NR&iJwKiOcEID4fN{(bP8z1lv)i16vwLk%C)YnXXF z-;BI`jx54`oxfH=isk*Lr@d5Ahq!$#fQ?YXtxMx0QX5O*vm9-{UL@lexWMW)ogKZ~ z-~EJhF!$y0voO|nc?mCrLD5+?J?>`tT4Uo+FU%k$0^_Q~I_wDY*fL_y;KQiT@~@0# zAenLoif{J=?LXUuamhr21mmAoz?GL+>nUfOMQn8D=g=2k)i6fuVWdekd=KZ&SRwnPUnwxMb2!E zw9x%?o?^GWtK4nci^zt6#P1DTq*9qJ&F8IMs_4%(tYscin(STTi!cV+))e*^=jLU` zqGTw%uIbxc)h_4}PwZL)>zjvnEI@VPl7fT>LPGcCsnOHrS!C3I4ay96k?~Dvb_Mod z-7!$6E=rI1X<9>Vy=ZqK75wwl`oLuQyO`D>{=&k+qZv%W2wG6KShOdxte?nbC{uK$ z`m9O^jm>gGz2Q*OX!CID;&)O zvNGh@Y_a)}wgt4*!hA3A(Ig1lYcnejVvGcF^C73?pt0d#W&GaPN`gSawpR6C=m|IOHlrTHg9Hs20D?U2MF{KGKJlnqyE|G(A{zG zz2igCB*M8tsI%7Hog}OGw4l7;gtT;0dMzSByda~%bMD%MjOve(B~9ANbdM=vSxh36 z7oVs3c@lE2eORINAgk@gdfRI{gOBWJo3B0E&2s!hL+|C7lPd)rCbD;dcv~l@Q}Gm= zDB~?-eY=x-)+1WeBQl;{@T#Z3iVu^-6~le?gZxuP{b9#lXiIbN)UuLlnVES|CBkf` zpK~CkDqzO(6Sf39P}dX8%W3)!vt`@bERHvwp*(@XcBV~Bu0|TXiFV4#R$Z3!`-Ecgm!$Aa{ zk8K4glJOT7@9{WjJRCOG>%p08AkkfqVKZPj`oe6?CDlYO@ubXV&ikBgBDA+WLU+`x zm5XWNv1f5uu>P%>GQCdqC{Xe;GN22p9oesNxlqT7SCVzw(+K6XI|F!zyC`44Cu_t* zvXTym!OYSxF}2k``v9cf`Vfm_!Hy5^y!j3I!75vG=lJ@%zfKcw9kPg3>Bsym8qn4>-_(QW5H#IF2+RZ?1x?sncNP-G!l3P!Fva=MwLxlo``m*$ox1%@g(o6`x zvZ`2@7}QGs?i3nR8XH=8sH;&wvptymOCu6*@}Qi1IGc$E3qybt{^pE-feZL02mucK zWBdJi-bCd8$21gVK!QJ^^dIx?e*=}JH?@^EUPD|ae&Q{C!w9|q3M!ZjVWKG4lKb=I z$a1z&oE``P!iG3OXkJUnrV9WSysvkjIhQ*}E>kyhS1M~thN@0i!wU&}yj_x641tvk z4~ehjN#SyG?pZVxCjRHDtmD8YRHIb4TXyWv?R3v|Di{0PS~pp$v^&@jmkwI9r>WEF zYZv<+Nqxor1?j@xcHSXVJ#CIz0TR?N1MvklYw^}j8OgN?=a7>2^rA1494>p#yR*2w zqW#5XKIwzeoRz3OwVsN`6l|01rDcAC<`)SI!io$Jr*c31SMp6#;Tlh_Te7v{slX=$Md52NtF6D99 zIt4qI{$W*(Uk8b%AP+Cz7-Lm=W}$q250_UjVSITnu{Dn4(HyQ4CHrZKoh{5tE&6Rc zqo<4i=B&oAxM%dQ`#v07xM0jswQriOI20EalVp|~X+D6CvfqLA1-XT~Y-V+{`a{t4 zZ!NG(XC3o`Q{ZR`2QJ5oR~DBQTFtyuDzDmBi`_FdNXL19!$Y+)H}+^9Xcj@ppCjI? zR{*BQ=WJO~Ax*<%vt|IC1b4Qt9_O>S4^;M7r;y@5A89E7gc?bK8*#zo@Dx9GZ&mJ6zBTZ%|#5Y4XUoCsI1*)uSeQZI-of?g`LS ziR-?|Yi?R(>5@BfcYbac_K~C|Ekz>d>CYz2d)F$%VzXCIXLYgt-Iu(A zF%v)Sj$A0iI;%QE<4gE$-B{o2o54ITQ{yPj&ho_fyOa}&eLIu|qbH?l5VEQ`rF_#s zQ(Y0zJeEHu&nUgrk~Q=Bd>#}Zj)Jvj+~RRI{HSf1Z{{3O*)(nhh*#c*Z=||Cu6!L_ zkfHl>tMGh!-kxk_#}Gb#VSGYp)|R+2tNg6$1O6MsaVaz{fw!vH3`TgoFIRBNs5ZzhWi(&0XjH_ii2IjqsTFB$ouDQK()rPIo(Od-ye0rG>QiY}>V}DDkS9eZYgUUWqI|VvlZN4*@ z&z?rAvQ-EDu8QM6S=aWtU2msmvOBk?hxufkSox*r+fQkRb%r8q0Q=>P-x~?vADre8 zBd-trPyD(rOZljMbw_(wuTH<7+~qBjX~56#p55{~PB~a|XV9E{g(fthN0to%E+r}-y%-rv3DT31r9<5# zcf4-=J;jbO>qctS%vj?jzB*673az8b4C}eND87yOtJFDu#?TEcZOozFwVah zEh(KCJ%|<+Pv4atJ4VCgkakb1OdNQaE^7#jNQC*p^#ASzLtrW}$$umTj2A>@6ilTH z=^AzUk}`rymbncgDE3cs81K&y!TNg)www Oi}QF-1I&N^_J082g(BYo literal 0 HcmV?d00001 diff --git a/test/fixtures/keys/ec-fips.pfx b/test/fixtures/keys/ec-fips.pfx new file mode 100644 index 0000000000000000000000000000000000000000..1c26d641b9fc78bc68993098dee50019150d6390 GIT binary patch literal 1261 zcmXqLVtL8L$ZXKW62!)-)#lOmotKfFaX}M{GfNYTJy6)%po!@fLW*rc6Vq*=&~+w8 z1|W3-A;V}859hG*8Uz^_BDg#TDl7s|lDkUY3*M~$d)_kP?ddrOn3y;i40zZ$Aht4b zvN9ORvT-J~c`&9jvoLD02zXmWU1#}p;NYjTtLzUuiWN3AF(qi@7lRJU)=;!=C!07r5l)TNC*s1^t-Y%Ea=Oj$r^n2NA7L>HcPa@p{a|1$;ZgE zA7zdwuC09kMB`dT{(}i$gaae^KjyiAZkulREjY77e@P?j``ZUL+-7cCrMGL}Tc6D6 zpKs?*W^TIhEX#Kp&&5ur-omhhJvOWhw^;1*Wq-A=DEd~X{zSF+Wooyp7 z>FGLK=WKrS*FV*L9)`TDKcwdR$g)nd`DD3r}lNfh56-F4xh`e zf{_PL)EXtbe_&~}dBYMLye(u&g6bJ><7IId7HgXO9?d-@`TN(JlRxEWtu?sZE~-_o zS+#`a2xnRD!X497xh9J|Jb8CRjYiX@V=af~M92Fc{=M(W#lH3vq4PSrBUu03-_a@_ zS|8f5PI7D7$;f~DQ!LLrDX#m!<&5sz+eh~58V7b(m#Kd}em0`*TiDtkzjJuH9+;{e zyBui6;$RO@D(d=NE;k0vEsTS>BA=g6>q;cobRC=^oX1!B zL}6m>eLLfi>aO;Qq83j#CeGaAWdGw|u$5gS7yq|{36e&4R6;IKvE864z+#cf!C=gr zSs1tC*VDa*TXk!7XQ@?rUkG#jc45N7)!K_Q6?|<}oqj6nesD?_|=GK8@fzb`gw*oeQ zJU27p}){EC33o5$^y1 literal 0 HcmV?d00001 diff --git a/test/parallel/test-crypto-argon2-job.js b/test/parallel/test-crypto-argon2-job.js index 7c16f9d850af..b3c04e8927e5 100644 --- a/test/parallel/test-crypto-argon2-job.js +++ b/test/parallel/test-crypto-argon2-job.js @@ -4,10 +4,12 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3, 2)) common.skip('requires OpenSSL >= 3.2'); +if (hasFIPS(3)) + common.skip('Argon2 is not available in FIPS mode'); // Exercises the native Argon2 job directly via internalBinding, bypassing // the JS validators, to ensure that if invalid parameters ever reach the diff --git a/test/parallel/test-crypto-argon2.js b/test/parallel/test-crypto-argon2.js index 1f238e61a61d..98367bab5471 100644 --- a/test/parallel/test-crypto-argon2.js +++ b/test/parallel/test-crypto-argon2.js @@ -3,7 +3,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3, 2)) common.skip('requires OpenSSL >= 3.2'); @@ -28,6 +28,19 @@ const secret = Buffer.alloc(8, 0x03); const associatedData = Buffer.alloc(12, 0x04); const defaults = { message, nonce, parallelism: 1, tagLength: 64, memory: 8, passes: 3 }; +if (hasFIPS(3)) { + assert.throws(() => crypto.argon2Sync('argon2id', defaults), { + name: 'Error', + message: /:digital envelope routines::unsupported$/, + }); + crypto.argon2('argon2id', defaults, common.mustCall((err, result) => { + assert.strictEqual(err?.name, 'Error'); + assert.match(err.message, /:digital envelope routines::unsupported$/); + assert.strictEqual(result, undefined); + })); + return; +} + const good = [ // Test vectors from RFC 9106 https://www.rfc-editor.org/rfc/rfc9106.html#name-test-vectors // and OpenSSL 3.2 https://github.com/openssl/openssl/blob/6dfa998f7ea150f9c6d4e4727cf6d5c82a68a8da/test/recipes/30-test_evp_data/evpkdf_argon2.txt diff --git a/test/parallel/test-crypto-async-sign-verify.js b/test/parallel/test-crypto-async-sign-verify.js index 2fb17748324f..7adc2d080e78 100644 --- a/test/parallel/test-crypto-async-sign-verify.js +++ b/test/parallel/test-crypto-async-sign-verify.js @@ -3,12 +3,14 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); const assert = require('assert'); const util = require('util'); const crypto = require('crypto'); const fixtures = require('../common/fixtures'); +const fips3 = hasFIPS(3); + function test( publicFixture, privateFixture, @@ -65,6 +67,17 @@ function test( } } +function testSignFailure(privateFixture, algorithm, options, code, message) { + const key = { key: fixtures.readKey(privateFixture), ...options }; + const data = Buffer.from('Hello world'); + assert.throws(() => crypto.sign(algorithm, data, key), { code }); + crypto.sign(algorithm, data, key, common.mustCall((err) => { + // Async crypto jobs in v24 preserve the OpenSSL message without a code. + assert.strictEqual(err?.name, 'Error'); + assert.strictEqual(err.message, message); + })); +} + // RSA w/ default padding test('rsa_public.pem', 'rsa_private.pem', 'sha256', true); test('rsa_public.pem', 'rsa_private.pem', 'sha256', true, @@ -94,14 +107,20 @@ if (!process.features.openssl_is_boringssl) { test('ed448_public.pem', 'ed448_private.pem', undefined, true); // ECDSA w/ der signature encoding - test('ec_secp256k1_public.pem', 'ec_secp256k1_private.pem', 'sha384', - false); - test('ec_secp256k1_public.pem', 'ec_secp256k1_private.pem', 'sha384', - false, { dsaEncoding: 'der' }); - - // ECDSA w/ ieee-p1363 signature encoding - test('ec_secp256k1_public.pem', 'ec_secp256k1_private.pem', 'sha384', false, - { dsaEncoding: 'ieee-p1363' }); + if (fips3) { + testSignFailure('ec_secp256k1_private.pem', 'sha384', {}, + 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE', + 'error:03000096:digital envelope routines::operation not supported for this keytype'); + } else { + test('ec_secp256k1_public.pem', 'ec_secp256k1_private.pem', 'sha384', + false); + test('ec_secp256k1_public.pem', 'ec_secp256k1_private.pem', 'sha384', + false, { dsaEncoding: 'der' }); + + // ECDSA w/ ieee-p1363 signature encoding + test('ec_secp256k1_public.pem', 'ec_secp256k1_private.pem', 'sha384', false, + { dsaEncoding: 'ieee-p1363' }); + } // DSA w/ der signature encoding test('dsa_public.pem', 'dsa_private.pem', 'sha256', @@ -156,7 +175,7 @@ MCowBQYDK2VuAyEA6pwGRbadNQAI/tYN8+/p/0/hbsdHfOEGr1ADiLVk/Gc= const signature = crypto.randomBytes(16); let expected = /no default digest/; - if (hasOpenSSL3 || process.features.openssl_is_boringssl) { + if (hasOpenSSL(3) || process.features.openssl_is_boringssl) { expected = /operation[\s_]not[\s_]supported[\s_]for[\s_]this[\s_]keytype/i; } @@ -167,11 +186,21 @@ MCowBQYDK2VuAyEA6pwGRbadNQAI/tYN8+/p/0/hbsdHfOEGr1ADiLVk/Gc= } { - const { privateKey } = crypto.generateKeyPairSync('rsa', { - modulusLength: 512 - }); - crypto.sign('sha512', 'message', privateKey, common.mustCall((err) => { - assert.ok(err); - assert.match(err.message, /digest[\s_]too[\s_]big[\s_]for[\s_]rsa[\s_]key/i); - })); + if (fips3) { + crypto.generateKeyPair('rsa', { modulusLength: 512 }, + common.mustCall((err) => { + assert.strictEqual(err?.name, 'Error'); + assert.strictEqual( + err.message, 'error:020000AE:rsa routines::invalid modulus'); + })); + } else { + const { privateKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 512 + }); + crypto.sign('sha512', 'message', privateKey, common.mustCall((err) => { + assert.ok(err); + assert.match( + err.message, /digest[\s_]too[\s_]big[\s_]for[\s_]rsa[\s_]key/i); + })); + } } diff --git a/test/parallel/test-crypto-authenticated-stream.js b/test/parallel/test-crypto-authenticated-stream.js index 51b928ec36be..45736d3303dc 100644 --- a/test/parallel/test-crypto-authenticated-stream.js +++ b/test/parallel/test-crypto-authenticated-stream.js @@ -6,6 +6,7 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); +const { hasFIPS } = require('../common/crypto'); const fs = require('fs'); const stream = require('stream'); const tmpdir = require('../common/tmpdir'); @@ -120,6 +121,16 @@ function test(config) { return; } + if (hasFIPS(3)) { + assert.throws(() => crypto.createDecipheriv( + config.cipher, config.key, config.iv, { + authTagLength: config.authTagLength, + }), { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + }); + return; + } + direct(config); mstream(config); fstream(config); diff --git a/test/parallel/test-crypto-authenticated.js b/test/parallel/test-crypto-authenticated.js index db1d12ae6711..3ad34729f871 100644 --- a/test/parallel/test-crypto-authenticated.js +++ b/test/parallel/test-crypto-authenticated.js @@ -29,9 +29,10 @@ const assert = require('assert'); const crypto = require('crypto'); const { inspect } = require('util'); const fixtures = require('../common/fixtures'); -const { hasOpenSSL, hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); -const isFipsEnabled = crypto.getFips(); +const isFipsEnabled = crypto.getFips() === 1; +const fips3 = hasFIPS(3); // // Test authenticated encryption modes. @@ -718,6 +719,14 @@ for (const test of TEST_CASES) { const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]); const tag = cipher.getAuthTag(); + if (fips3 && mode === 'ccm') { + assert.throws(() => crypto.createDecipheriv( + `aes-128-${mode}`, key, iv, opts), { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + }); + continue; + } + const decipher = crypto.createDecipheriv(`aes-128-${mode}`, key, iv, opts); decipher.setAuthTag(tag); assert.throws(() => { @@ -804,7 +813,7 @@ for (const test of TEST_CASES) { } catch (err) { // OpenSSL without https://github.com/openssl/openssl/pull/32427 // cannot finalize an empty CCM message unless update() was called. - if (hasOpenSSL3) { + if (hasOpenSSL(3)) { assert.strictEqual(err.code, 'ERR_OSSL_TAG_NOT_SET'); } else { assert.match(err.message, /Unsupported state/); @@ -818,7 +827,14 @@ for (const test of TEST_CASES) { } } -if (!process.features.openssl_is_boringssl) { +if (fips3) { + assert.throws(() => crypto.createCipheriv( + 'chacha20-poly1305', Buffer.alloc(32), Buffer.alloc(12), { + authTagLength: 16, + }), { + code: 'ERR_OSSL_EVP_UNSUPPORTED', + }); +} else if (!process.features.openssl_is_boringssl) { const key = Buffer.alloc(32); const iv = Buffer.alloc(12); @@ -836,7 +852,7 @@ if (!process.features.openssl_is_boringssl) { // ChaCha20-Poly1305 should respect the authTagLength option and should not // require the authentication tag before calls to update() during decryption. -if (!process.features.openssl_is_boringssl) { +if (!fips3 && !process.features.openssl_is_boringssl) { const key = Buffer.alloc(32); const iv = Buffer.alloc(12); @@ -887,7 +903,7 @@ if (!process.features.openssl_is_boringssl) { // shorter tags as long as their length was valid according to NIST SP 800-38D. // For ChaCha20-Poly1305, we intentionally deviate from that because there are // no recommended or approved authentication tag lengths below 16 bytes. -if (!process.features.openssl_is_boringssl) { +if (!fips3 && !process.features.openssl_is_boringssl) { const rfcTestCases = TEST_CASES.filter(({ algo, tampered }) => { return algo === 'chacha20-poly1305' && tampered === false; }); @@ -926,7 +942,7 @@ if (!process.features.openssl_is_boringssl) { } // https://github.com/nodejs/node/issues/45874 -if (!process.features.openssl_is_boringssl) { +if (!fips3 && !process.features.openssl_is_boringssl) { const rfcTestCases = TEST_CASES.filter(({ algo, tampered }) => { return algo === 'chacha20-poly1305' && tampered === false; }); @@ -973,7 +989,7 @@ if (ciphers.includes('aes-128-ccm')) { const tag = cipher.getAuthTag(); assert.strictEqual(tag.length, 16); - if (isFipsEnabled && hasOpenSSL3) { + if (fips3) { assert.throws(() => crypto.createDecipheriv( 'aes-128-ccm', key, nonce, { authTagLength: 16 }), { code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', diff --git a/test/parallel/test-crypto-certificate.js b/test/parallel/test-crypto-certificate.js index 28d20ba61c75..6462654680ea 100644 --- a/test/parallel/test-crypto-certificate.js +++ b/test/parallel/test-crypto-certificate.js @@ -26,6 +26,7 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); +const { hasFIPS } = require('../common/crypto'); const { Certificate } = crypto; const fixtures = require('../common/fixtures'); @@ -42,7 +43,7 @@ function copyArrayBuffer(buf) { function checkMethods(certificate) { if (!process.features.openssl_is_boringssl) - assert.strictEqual(certificate.verifySpkac(spkacValid), true); + assert.strictEqual(certificate.verifySpkac(spkacValid), !hasFIPS(3)); assert.strictEqual(certificate.verifySpkac(spkacFail), false); assert.strictEqual( @@ -59,9 +60,10 @@ function checkMethods(certificate) { if (!process.features.openssl_is_boringssl) { const ab = copyArrayBuffer(spkacValid); - assert.strictEqual(certificate.verifySpkac(ab), true); - assert.strictEqual(certificate.verifySpkac(new Uint8Array(ab)), true); - assert.strictEqual(certificate.verifySpkac(new DataView(ab)), true); + const expected = !hasFIPS(3); + assert.strictEqual(certificate.verifySpkac(ab), expected); + assert.strictEqual(certificate.verifySpkac(new Uint8Array(ab)), expected); + assert.strictEqual(certificate.verifySpkac(new DataView(ab)), expected); } } diff --git a/test/parallel/test-crypto-cipheriv-decipheriv.js b/test/parallel/test-crypto-cipheriv-decipheriv.js index 672dc75446c1..4c2a0c1fd1cd 100644 --- a/test/parallel/test-crypto-cipheriv-decipheriv.js +++ b/test/parallel/test-crypto-cipheriv-decipheriv.js @@ -5,8 +5,9 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); -const isFipsEnabled = crypto.getFips(); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); +const isFipsEnabled = crypto.getFips() === 1; +const fips3 = hasFIPS(3); function testCipher1(key, iv) { // Test encryption and decryption with explicit key and iv @@ -164,10 +165,12 @@ function testSm4Xts() { { const Cipheriv = crypto.Cipheriv; - const key = '123456789012345678901234'; - const iv = '12345678'; + const algorithm = fips3 ? 'aes-128-cbc' : 'des-ede3-cbc'; + const key = fips3 ? + '1234567890123456' : '123456789012345678901234'; + const iv = fips3 ? '1234567890123456' : '12345678'; - const instance = Cipheriv('des-ede3-cbc', key, iv); + const instance = Cipheriv(algorithm, key, iv); assert(instance instanceof Cipheriv, 'Cipheriv is expected to return a new ' + 'instance when called without `new`'); @@ -197,10 +200,12 @@ function testSm4Xts() { { const Decipheriv = crypto.Decipheriv; - const key = '123456789012345678901234'; - const iv = '12345678'; + const algorithm = fips3 ? 'aes-128-cbc' : 'des-ede3-cbc'; + const key = fips3 ? + '1234567890123456' : '123456789012345678901234'; + const iv = fips3 ? '1234567890123456' : '12345678'; - const instance = Decipheriv('des-ede3-cbc', key, iv); + const instance = Decipheriv(algorithm, key, iv); assert(instance instanceof Decipheriv, 'Decipheriv expected to return a new' + ' instance when called without `new`'); @@ -231,8 +236,10 @@ function testSm4Xts() { testCipher1('0123456789abcd0123456789', '12345678'); testCipher1('0123456789abcd0123456789', Buffer.from('12345678')); testCipher1(Buffer.from('0123456789abcd0123456789'), '12345678'); -testCipher1(Buffer.from('0123456789abcd0123456789'), Buffer.from('12345678')); -testCipher2(Buffer.from('0123456789abcd0123456789'), Buffer.from('12345678')); +testCipher1( + Buffer.from('0123456789abcd0123456789'), Buffer.from('12345678')); +testCipher2( + Buffer.from('0123456789abcd0123456789'), Buffer.from('12345678')); if (!isFipsEnabled) { testCipher3(Buffer.from('000102030405060708090A0B0C0D0E0F', 'hex'), @@ -286,8 +293,8 @@ assert.throws( errMessage); // But all other IV lengths should be accepted. -const minIvLength = hasOpenSSL3 ? 8 : 1; -const maxIvLength = hasOpenSSL3 ? 64 : 256; +const minIvLength = hasOpenSSL(3) ? 8 : 1; +const maxIvLength = hasOpenSSL(3) ? 64 : 256; for (let n = minIvLength; n < maxIvLength; n += 1) { if (isFipsEnabled && n < 12) continue; crypto.createCipheriv('aes-128-gcm', Buffer.alloc(16), Buffer.alloc(n)); diff --git a/test/parallel/test-crypto-classes.js b/test/parallel/test-crypto-classes.js index 429bc91d4412..48d68c93fb62 100644 --- a/test/parallel/test-crypto-classes.js +++ b/test/parallel/test-crypto-classes.js @@ -6,7 +6,7 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); // 'ClassName' : ['args', 'for', 'constructor'] const TEST_CASES = { @@ -21,8 +21,17 @@ const TEST_CASES = { 'ECDH': ['prime256v1'], }; -if (!crypto.getFips()) { - TEST_CASES.DiffieHellman = [hasOpenSSL3 ? 1024 : 256]; +if (hasFIPS(3)) { + TEST_CASES.Hmac = ['sha1', '0123456789abcdef']; + TEST_CASES.Cipheriv = [ + 'aes-128-cbc', '0123456789abcdef', '1234567890abcdef']; + TEST_CASES.Decipheriv = TEST_CASES.Cipheriv; + TEST_CASES.Sign = ['RSA-SHA256']; + TEST_CASES.Verify = ['RSA-SHA256']; + TEST_CASES.DiffieHellman = [2048]; + TEST_CASES.DiffieHellmanGroup = ['modp14']; +} else if (crypto.getFips() !== 1) { + TEST_CASES.DiffieHellman = [hasOpenSSL(3) ? 1024 : 256]; } for (const [clazz, args] of Object.entries(TEST_CASES)) { diff --git a/test/parallel/test-crypto-dh-constructor.js b/test/parallel/test-crypto-dh-constructor.js index eb8674932484..28747ac3a726 100644 --- a/test/parallel/test-crypto-dh-constructor.js +++ b/test/parallel/test-crypto-dh-constructor.js @@ -5,9 +5,10 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); -const size = crypto.getFips() || hasOpenSSL3 ? 1024 : 256; +const size = hasFIPS(3) ? + 2048 : (crypto.getFips() === 1 || hasOpenSSL(3) ? 1024 : 256); const dh1 = crypto.createDiffieHellman(size); const p1 = dh1.getPrime('buffer'); @@ -21,7 +22,7 @@ const p1 = dh1.getPrime('buffer'); { const DiffieHellmanGroup = crypto.DiffieHellmanGroup; - const dhg = DiffieHellmanGroup('modp5'); + const dhg = DiffieHellmanGroup(hasFIPS(3) ? 'modp14' : 'modp5'); assert(dhg instanceof DiffieHellmanGroup, 'DiffieHellmanGroup is expected ' + 'to return a new instance when ' + 'called without `new`'); diff --git a/test/parallel/test-crypto-dh-curves.js b/test/parallel/test-crypto-dh-curves.js index 68668ae6a284..8021d3f7ad61 100644 --- a/test/parallel/test-crypto-dh-curves.js +++ b/test/parallel/test-crypto-dh-curves.js @@ -5,7 +5,7 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); const { DH_CHECK_P_NOT_PRIME, DH_CHECK_P_NOT_SAFE_PRIME, @@ -123,120 +123,126 @@ if (availableCurves.has('prime256v1') && availableCurves.has('secp256k1')) { // ECDH should check that point is on curve const ecdh3 = crypto.createECDH('secp256k1'); - const key3 = ecdh3.generateKeys(); - - assert.throws( - () => ecdh2.computeSecret(key3, 'latin1', 'buffer'), - { - code: 'ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY', - name: 'Error', - message: 'Public key is not valid for specified curve' + if (hasFIPS(3)) { + assert.throws(() => ecdh3.generateKeys(), { + code: 'ERR_CRYPTO_OPERATION_FAILED', }); + } else { + const key3 = ecdh3.generateKeys(); - // ECDH should allow .setPrivateKey()/.setPublicKey() - const ecdh4 = crypto.createECDH('prime256v1'); + assert.throws( + () => ecdh2.computeSecret(key3, 'latin1', 'buffer'), + { + code: 'ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY', + name: 'Error', + message: 'Public key is not valid for specified curve' + }); - ecdh4.setPrivateKey(ecdh1.getPrivateKey()); - ecdh4.setPublicKey(ecdh1.getPublicKey()); + // ECDH should allow .setPrivateKey()/.setPublicKey() + const ecdh4 = crypto.createECDH('prime256v1'); - const ecdh4Secret = ecdh4.computeSecret(ecdh2.getPublicKey()); - assert.deepStrictEqual(ecdh4.computeSecret(ecdh2.getPublicKey()), - ecdh4Secret); + ecdh4.setPrivateKey(ecdh1.getPrivateKey()); + ecdh4.setPublicKey(ecdh1.getPublicKey()); - assert.throws(() => { - ecdh4.setPublicKey(ecdh3.getPublicKey()); - }, { message: 'Failed to convert Buffer to EC_POINT' }); - assert.deepStrictEqual(ecdh4.computeSecret(ecdh2.getPublicKey()), - ecdh4Secret); + const ecdh4Secret = ecdh4.computeSecret(ecdh2.getPublicKey()); + assert.deepStrictEqual(ecdh4.computeSecret(ecdh2.getPublicKey()), + ecdh4Secret); - // Verify that we can use ECDH without having to use newly generated keys. - const ecdh5 = crypto.createECDH('secp256k1'); + assert.throws(() => { + ecdh4.setPublicKey(ecdh3.getPublicKey()); + }, { message: 'Failed to convert Buffer to EC_POINT' }); + assert.deepStrictEqual(ecdh4.computeSecret(ecdh2.getPublicKey()), + ecdh4Secret); - // Verify errors are thrown when retrieving keys from an uninitialized object. - assert.throws(() => { - ecdh5.getPublicKey(); - }, /^Error: Failed to get ECDH public key$/); + // Verify that we can use ECDH without having to use newly generated keys. + const ecdh5 = crypto.createECDH('secp256k1'); - assert.throws(() => { - ecdh5.getPrivateKey(); - }, /^Error: Failed to get ECDH private key$/); + // Verify errors are thrown when retrieving keys from an uninitialized object. + assert.throws(() => { + ecdh5.getPublicKey(); + }, /^Error: Failed to get ECDH public key$/); - // A valid private key for the secp256k1 curve. - const cafebabeKey = 'cafebabe'.repeat(8); - // Associated compressed and uncompressed public keys (points). - const cafebabePubPtComp = + assert.throws(() => { + ecdh5.getPrivateKey(); + }, /^Error: Failed to get ECDH private key$/); + + // A valid private key for the secp256k1 curve. + const cafebabeKey = 'cafebabe'.repeat(8); + // Associated compressed and uncompressed public keys (points). + const cafebabePubPtComp = '03672a31bfc59d3f04548ec9b7daeeba2f61814e8ccc40448045007f5479f693a3'; - const cafebabePubPtUnComp = + const cafebabePubPtUnComp = '04672a31bfc59d3f04548ec9b7daeeba2f61814e8ccc40448045007f5479f693a3' + '2e02c7f93d13dc2732b760ca377a5897b9dd41a1c1b29dc0442fdce6d0a04d1d'; - ecdh5.setPrivateKey(cafebabeKey, 'hex'); - assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); - // Show that the public point (key) is generated while setting the - // private key. - assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); - - // Compressed and uncompressed public points/keys for other party's - // private key. - // 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF - const peerPubPtComp = + ecdh5.setPrivateKey(cafebabeKey, 'hex'); + assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); + // Show that the public point (key) is generated while setting the + // private key. + assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); + + // Compressed and uncompressed public points/keys for other party's + // private key. + // 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF + const peerPubPtComp = '02c6b754b20826eb925e052ee2c25285b162b51fdca732bcf67e39d647fb6830ae'; - const peerPubPtUnComp = + const peerPubPtUnComp = '04c6b754b20826eb925e052ee2c25285b162b51fdca732bcf67e39d647fb6830ae' + 'b651944a574a362082a77e3f2b5d9223eb54d7f2f76846522bf75f3bedb8178e'; - const sharedSecret = + const sharedSecret = '1da220b5329bbe8bfd19ceef5a5898593f411a6f12ea40f2a8eead9a5cf59970'; - assert.strictEqual(ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'), - sharedSecret); - assert.strictEqual(ecdh5.computeSecret(peerPubPtUnComp, 'hex', 'hex'), - sharedSecret); - assert.strictEqual(ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'), - sharedSecret); + assert.strictEqual(ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'), + sharedSecret); + assert.strictEqual(ecdh5.computeSecret(peerPubPtUnComp, 'hex', 'hex'), + sharedSecret); + assert.strictEqual(ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'), + sharedSecret); - // Verify that we still have the same key pair as before the computation. - assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); - assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); - - // Verify setting and getting compressed and non-compressed serializations. - ecdh5.setPublicKey(cafebabePubPtComp, 'hex'); - assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); - assert.strictEqual( - ecdh5.getPublicKey('hex', 'compressed'), - cafebabePubPtComp - ); - ecdh5.setPublicKey(cafebabePubPtUnComp, 'hex'); - assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); - assert.strictEqual( - ecdh5.getPublicKey('hex', 'compressed'), - cafebabePubPtComp - ); - - // Show why allowing the public key to be set on this type - // does not make sense. - ecdh5.setPublicKey(peerPubPtComp, 'hex'); - assert.strictEqual(ecdh5.getPublicKey('hex'), peerPubPtUnComp); - assert.throws(() => { - // Error because the public key does not match the private key anymore. - ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'); - }, /Invalid key pair/); - - // Set to a valid key to show that later attempts to set an invalid key are - // rejected. - ecdh5.setPrivateKey(cafebabeKey, 'hex'); - - // Some invalid private keys for the secp256k1 curve. - const errMessage = /Private key is not valid for specified curve/; - ['0000000000000000000000000000000000000000000000000000000000000000', - 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', - 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', - ].forEach((element) => { - assert.throws(() => { - ecdh5.setPrivateKey(element, 'hex'); - }, errMessage); - // Verify object state did not change. + // Verify that we still have the same key pair as before the computation. assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); - }); + assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); + + // Verify setting and getting compressed and non-compressed serializations. + ecdh5.setPublicKey(cafebabePubPtComp, 'hex'); + assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); + assert.strictEqual( + ecdh5.getPublicKey('hex', 'compressed'), + cafebabePubPtComp + ); + ecdh5.setPublicKey(cafebabePubPtUnComp, 'hex'); + assert.strictEqual(ecdh5.getPublicKey('hex'), cafebabePubPtUnComp); + assert.strictEqual( + ecdh5.getPublicKey('hex', 'compressed'), + cafebabePubPtComp + ); + + // Show why allowing the public key to be set on this type + // does not make sense. + ecdh5.setPublicKey(peerPubPtComp, 'hex'); + assert.strictEqual(ecdh5.getPublicKey('hex'), peerPubPtUnComp); + assert.throws(() => { + // Error because the public key does not match the private key anymore. + ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'); + }, /Invalid key pair/); + + // Set to a valid key to show that later attempts to set an invalid key are + // rejected. + ecdh5.setPrivateKey(cafebabeKey, 'hex'); + + // Some invalid private keys for the secp256k1 curve. + const errMessage = /Private key is not valid for specified curve/; + ['0000000000000000000000000000000000000000000000000000000000000000', + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', + ].forEach((element) => { + assert.throws(() => { + ecdh5.setPrivateKey(element, 'hex'); + }, errMessage); + // Verify object state did not change. + assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey); + }); + } } // Use of invalid keys was not cleaning up ERR stack, and was causing diff --git a/test/parallel/test-crypto-dh-generate-keys.js b/test/parallel/test-crypto-dh-generate-keys.js index acf7e2d09b2b..d074ba957516 100644 --- a/test/parallel/test-crypto-dh-generate-keys.js +++ b/test/parallel/test-crypto-dh-generate-keys.js @@ -6,10 +6,11 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); { - const size = crypto.getFips() || hasOpenSSL3 ? 1024 : 256; + const size = hasFIPS(3) ? + 2048 : (crypto.getFips() === 1 || hasOpenSSL(3) ? 1024 : 256); function unlessInvalidState(f) { try { diff --git a/test/parallel/test-crypto-dh-leak.js b/test/parallel/test-crypto-dh-leak.js index df1ba89737c6..8d5141eef4b1 100644 --- a/test/parallel/test-crypto-dh-leak.js +++ b/test/parallel/test-crypto-dh-leak.js @@ -9,11 +9,12 @@ if (common.isASan) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); const before = process.memoryUsage.rss(); { - const size = crypto.getFips() || hasOpenSSL3 ? 1024 : 256; + const size = hasFIPS(3) ? + 2048 : (crypto.getFips() === 1 || hasOpenSSL(3) ? 1024 : 256); const dh = crypto.createDiffieHellman(size); const publicKey = dh.generateKeys(); const privateKey = dh.getPrivateKey(); diff --git a/test/parallel/test-crypto-dh-modp2-views.js b/test/parallel/test-crypto-dh-modp2-views.js index a28e615b7f35..e32c515e1536 100644 --- a/test/parallel/test-crypto-dh-modp2-views.js +++ b/test/parallel/test-crypto-dh-modp2-views.js @@ -5,24 +5,30 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { modp2buf } = require('../common/crypto'); +const { hasFIPS, modp2buf } = require('../common/crypto'); if (process.features.openssl_is_boringssl) { common.skip('Skipping unsupported Diffie-Hellman tests'); } -const modp2 = crypto.createDiffieHellmanGroup('modp2'); +if (hasFIPS(3)) { + assert.throws(() => crypto.createDiffieHellman(1024), { + code: 'ERR_INVALID_ARG_VALUE', + }); +} else { + const modp2 = crypto.createDiffieHellmanGroup('modp2'); -const views = common.getArrayBufferViews(modp2buf); -for (const buf of [modp2buf, ...views]) { - // Ensure specific generator (string with encoding) works as expected with - // any ArrayBufferViews as the first argument to createDiffieHellman(). - const exmodp2 = crypto.createDiffieHellman(buf, '02', 'hex'); - modp2.generateKeys(); - exmodp2.generateKeys(); - const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) - .toString('hex'); - const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) - .toString('hex'); - assert.strictEqual(modp2Secret, exmodp2Secret); + const views = common.getArrayBufferViews(modp2buf); + for (const buf of [modp2buf, ...views]) { + // Ensure specific generator (string with encoding) works as expected with + // any ArrayBufferViews as the first argument to createDiffieHellman(). + const exmodp2 = crypto.createDiffieHellman(buf, '02', 'hex'); + modp2.generateKeys(); + exmodp2.generateKeys(); + const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) + .toString('hex'); + const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) + .toString('hex'); + assert.strictEqual(modp2Secret, exmodp2Secret); + } } diff --git a/test/parallel/test-crypto-dh-modp2.js b/test/parallel/test-crypto-dh-modp2.js index eb262f235ff3..0bf36f93a39a 100644 --- a/test/parallel/test-crypto-dh-modp2.js +++ b/test/parallel/test-crypto-dh-modp2.js @@ -5,44 +5,51 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { modp2buf } = require('../common/crypto'); +const { hasFIPS, modp2buf } = require('../common/crypto'); if (process.features.openssl_is_boringssl) { common.skip('Skipping unsupported Diffie-Hellman tests'); } -const modp2 = crypto.createDiffieHellmanGroup('modp2'); +if (hasFIPS(3)) { + assert.throws(() => crypto.createDiffieHellman(1024), { + code: 'ERR_INVALID_ARG_VALUE', + }); +} else { + const modp2 = crypto.createDiffieHellmanGroup('modp2'); -{ + { // Ensure specific generator (buffer) works as expected. - const exmodp2 = crypto.createDiffieHellman(modp2buf, Buffer.from([2])); - modp2.generateKeys(); - exmodp2.generateKeys(); - const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) + const exmodp2 = crypto.createDiffieHellman(modp2buf, Buffer.from([2])); + modp2.generateKeys(); + exmodp2.generateKeys(); + const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) .toString('hex'); - const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) + const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) .toString('hex'); - assert.strictEqual(modp2Secret, exmodp2Secret); -} + assert.strictEqual(modp2Secret, exmodp2Secret); + } -{ + { // Ensure specific generator (string without encoding) works as expected. - const exmodp2 = crypto.createDiffieHellman(modp2buf, '\x02'); - exmodp2.generateKeys(); - const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) + const exmodp2 = crypto.createDiffieHellman(modp2buf, '\x02'); + exmodp2.generateKeys(); + const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) .toString('hex'); - const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) + const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) .toString('hex'); - assert.strictEqual(modp2Secret, exmodp2Secret); -} + assert.strictEqual(modp2Secret, exmodp2Secret); + } -{ + { // Ensure specific generator (numeric) works as expected. - const exmodp2 = crypto.createDiffieHellman(modp2buf, 2); - exmodp2.generateKeys(); - const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) + const exmodp2 = crypto.createDiffieHellman(modp2buf, 2); + exmodp2.generateKeys(); + const modp2Secret = modp2.computeSecret(exmodp2.getPublicKey()) .toString('hex'); - const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) + const exmodp2Secret = exmodp2.computeSecret(modp2.getPublicKey()) .toString('hex'); - assert.strictEqual(modp2Secret, exmodp2Secret); + assert.strictEqual(modp2Secret, exmodp2Secret); + } + } diff --git a/test/parallel/test-crypto-dh-odd-key.js b/test/parallel/test-crypto-dh-odd-key.js index fbe42be425ed..c96227770e3d 100644 --- a/test/parallel/test-crypto-dh-odd-key.js +++ b/test/parallel/test-crypto-dh-odd-key.js @@ -27,19 +27,24 @@ if (!common.hasCrypto) { const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); function test() { const odd = Buffer.alloc(39, 'A'); - const c = crypto.createDiffieHellman(hasOpenSSL3 ? 1024 : 32); + const size = hasFIPS(3) ? 2048 : (hasOpenSSL(3) ? 1024 : 32); + const c = crypto.createDiffieHellman(size); c.setPrivateKey(odd); c.generateKeys(); } -// FIPS requires a length of at least 1024 -if (!crypto.getFips()) { +if (hasFIPS(3)) { + test(); + assert.throws(() => crypto.createDiffieHellman(1024), { + code: 'ERR_INVALID_ARG_VALUE', + }); +} else if (crypto.getFips() !== 1) { test(); } else { - assert.throws(function() { test(); }, /key size too small/); + assert.throws(test, /key size too small/); } diff --git a/test/parallel/test-crypto-dh-shared.js b/test/parallel/test-crypto-dh-shared.js index 515405034d76..5e6db278324d 100644 --- a/test/parallel/test-crypto-dh-shared.js +++ b/test/parallel/test-crypto-dh-shared.js @@ -5,9 +5,11 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); +const { hasFIPS } = require('../common/crypto'); -const alice = crypto.createDiffieHellmanGroup('modp5'); -const bob = crypto.createDiffieHellmanGroup('modp5'); +const group = hasFIPS(3) ? 'modp14' : 'modp5'; +const alice = crypto.createDiffieHellmanGroup(group); +const bob = crypto.createDiffieHellmanGroup(group); alice.generateKeys(); bob.generateKeys(); const aSecret = alice.computeSecret(bob.getPublicKey()).toString('hex'); diff --git a/test/parallel/test-crypto-dh.js b/test/parallel/test-crypto-dh.js index 01a7f0f3ddd8..f5d239f1d457 100644 --- a/test/parallel/test-crypto-dh.js +++ b/test/parallel/test-crypto-dh.js @@ -7,11 +7,13 @@ if (!common.hasCrypto) { const assert = require('assert'); const crypto = require('crypto'); const { - hasOpenSSL3, + hasOpenSSL, + hasFIPS, } = require('../common/crypto'); { - const size = crypto.getFips() || hasOpenSSL3 ? 1024 : 256; + const size = hasFIPS(3) ? + 2048 : (crypto.getFips() === 1 || hasOpenSSL(3) ? 1024 : 256); const dh1 = crypto.createDiffieHellman(size); const p1 = dh1.getPrime('buffer'); const dh2 = crypto.createDiffieHellman(p1, 'buffer'); @@ -57,7 +59,7 @@ const { assert.strictEqual(secret1, secret4); let wrongBlockLength; - if (hasOpenSSL3) { + if (hasOpenSSL(3)) { wrongBlockLength = { message: /wrong[\s_]final[\s_]block[\s_]length/i, code: /ERR_OSSL_(EVP_)?WRONG_FINAL_BLOCK_LENGTH/, diff --git a/test/parallel/test-crypto-ecdh-convert-key.js b/test/parallel/test-crypto-ecdh-convert-key.js index c0046099df9e..8c910b1b6052 100644 --- a/test/parallel/test-crypto-ecdh-convert-key.js +++ b/test/parallel/test-crypto-ecdh-convert-key.js @@ -6,6 +6,7 @@ if (!common.hasCrypto) const assert = require('assert'); const { ECDH, createSign, getCurves } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); // A valid private key for the secp256k1 curve. const cafebabeKey = 'cafebabe'.repeat(8); @@ -93,11 +94,17 @@ if (getCurves().includes('secp256k1')) { // Compare to getPublicKey. const ecdh1 = ECDH('secp256k1'); - ecdh1.generateKeys(); - ecdh1.setPrivateKey(cafebabeKey, 'hex'); - assert.strictEqual(ecdh1.getPublicKey('hex', 'uncompressed'), uncompressed); - assert.strictEqual(ecdh1.getPublicKey('hex', 'compressed'), compressed); - assert.strictEqual(ecdh1.getPublicKey('hex', 'hybrid'), hybrid); + if (hasFIPS(3)) { + assert.throws(() => ecdh1.generateKeys(), { + code: 'ERR_CRYPTO_OPERATION_FAILED', + }); + } else { + ecdh1.generateKeys(); + ecdh1.setPrivateKey(cafebabeKey, 'hex'); + assert.strictEqual(ecdh1.getPublicKey('hex', 'uncompressed'), uncompressed); + assert.strictEqual(ecdh1.getPublicKey('hex', 'compressed'), compressed); + assert.strictEqual(ecdh1.getPublicKey('hex', 'hybrid'), hybrid); + } } // See https://github.com/nodejs/node/issues/26133, failed ConvertKey diff --git a/test/parallel/test-crypto-eddsa-variants.js b/test/parallel/test-crypto-eddsa-variants.js index 691534f13f5b..3a75a7bf4f6f 100644 --- a/test/parallel/test-crypto-eddsa-variants.js +++ b/test/parallel/test-crypto-eddsa-variants.js @@ -7,6 +7,7 @@ const assert = require('assert'); const crypto = require('crypto'); const { hasOpenSSL, + hasFIPS, } = require('../common/crypto'); // RFC 8032 Section 7 test vectors for Ed25519, Ed25519ctx, and Ed448. @@ -191,7 +192,18 @@ for (const v of vectors) { const signKey = context ? { key: privateKey, context } : privateKey; const verifyKey = context ? { key: publicKey, context } : publicKey; - const sig = crypto.sign(null, message, signKey); + let sig; + try { + sig = crypto.sign(null, message, signKey); + } catch (err) { + if (!hasFIPS(3) || + (!v.algorithm.endsWith('ctx') && !v.context)) { + throw err; + } + assert.strictEqual( + err.code, 'ERR_OSSL_INVALID_EDDSA_INSTANCE_FOR_ATTEMPTED_OPERATION'); + continue; + } assert.deepStrictEqual(sig, expectedSig); assert.strictEqual( crypto.verify(null, message, verifyKey, expectedSig), true); diff --git a/test/parallel/test-crypto-encap-decap.js b/test/parallel/test-crypto-encap-decap.js index 4b19d7179440..a6052701abe3 100644 --- a/test/parallel/test-crypto-encap-decap.js +++ b/test/parallel/test-crypto-encap-decap.js @@ -6,10 +6,11 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); const fixtures = require('../common/fixtures'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); const { promisify } = require('util'); const isBoringSSL = process.features.openssl_is_boringssl; +const isFips = hasFIPS(3); if (!hasOpenSSL(3) && !isBoringSSL) { assert.throws(() => crypto.encapsulate(), { code: 'ERR_CRYPTO_KEM_NOT_SUPPORTED' }); @@ -36,7 +37,7 @@ const keys = { privateKey: fixtures.readKey('rsa_pss_private_2048.pem', 'ascii'), }, 'p-256': { - supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 + supported: hasOpenSSL(3, 2) && !isFips, // DHKEM was added in 3.2 publicKey: fixtures.readKey('ec_p256_public.pem', 'ascii'), privateKey: fixtures.readKey('ec_p256_private.pem', 'ascii'), sharedSecretLength: 32, @@ -44,7 +45,7 @@ const keys = { raw: true, }, 'p-384': { - supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 + supported: hasOpenSSL(3, 2) && !isFips, // DHKEM was added in 3.2 publicKey: fixtures.readKey('ec_p384_public.pem', 'ascii'), privateKey: fixtures.readKey('ec_p384_private.pem', 'ascii'), sharedSecretLength: 48, @@ -52,7 +53,7 @@ const keys = { raw: true, }, 'p-521': { - supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 + supported: hasOpenSSL(3, 2) && !isFips, // DHKEM was added in 3.2 publicKey: fixtures.readKey('ec_p521_public.pem', 'ascii'), privateKey: fixtures.readKey('ec_p521_private.pem', 'ascii'), sharedSecretLength: 64, @@ -65,7 +66,7 @@ const keys = { privateKey: fixtures.readKey('ec_secp256k1_private.pem', 'ascii'), }, 'x25519': { - supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 + supported: hasOpenSSL(3, 2) && !isFips, // DHKEM was added in 3.2 publicKey: fixtures.readKey('x25519_public.pem', 'ascii'), privateKey: fixtures.readKey('x25519_private.pem', 'ascii'), sharedSecretLength: 32, @@ -73,7 +74,7 @@ const keys = { raw: true, }, 'x448': { - supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2 + supported: hasOpenSSL(3, 2) && !isFips, // DHKEM was added in 3.2 publicKey: fixtures.readKey('x448_public.pem', 'ascii'), privateKey: fixtures.readKey('x448_private.pem', 'ascii'), sharedSecretLength: 64, diff --git a/test/parallel/test-crypto-getcipherinfo.js b/test/parallel/test-crypto-getcipherinfo.js index 35eced3b296e..4da0874e596d 100644 --- a/test/parallel/test-crypto-getcipherinfo.js +++ b/test/parallel/test-crypto-getcipherinfo.js @@ -8,9 +8,9 @@ const { createCipheriv, createHash, getCiphers, - getCipherInfo + getCipherInfo, } = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL3, hasFIPS } = require('../common/crypto'); const assert = require('assert'); @@ -123,7 +123,10 @@ if (!process.features.openssl_is_boringssl) { } assert(!getCipherInfo('aes-128-ocb', { ivLength: 16 })); -if (!process.features.openssl_is_boringssl) { +if (hasFIPS(3)) { + assert.strictEqual( + getCipherInfo('aes-128-ocb', { ivLength: 12 }), undefined); +} else if (!process.features.openssl_is_boringssl) { for (let n = 1; n < 16; n++) assert(getCipherInfo('aes-128-ocb', { ivLength: n })); } else { diff --git a/test/parallel/test-crypto-hkdf.js b/test/parallel/test-crypto-hkdf.js index 80cf5e9aff5e..1e2069d2e701 100644 --- a/test/parallel/test-crypto-hkdf.js +++ b/test/parallel/test-crypto-hkdf.js @@ -13,7 +13,7 @@ const { hkdfSync, getHashes } = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); { assert.throws(() => hkdf(), { @@ -137,12 +137,12 @@ const { hasOpenSSL3 } = require('../common/crypto'); } const algorithms = [ - ['sha256', 'secret', 'salt', 'info', 10], + ['sha256', '0123456789abcdef', '0123456789abcdef', 'info', 10], ['sha256', '', '', '', 10], ['sha256', '', 'salt', '', 10], ['sha512', 'secret', 'salt', '', 15], ]; -if (!hasOpenSSL3 && !process.features.openssl_is_boringssl) +if (!hasOpenSSL(3) && !process.features.openssl_is_boringssl) algorithms.push(['whirlpool', 'secret', '', 'info', 20]); algorithms.forEach(([ hash, secret, salt, info, length ]) => { @@ -233,7 +233,7 @@ algorithms.forEach(([ hash, secret, salt, info, length ]) => { }); -if (!hasOpenSSL3) { +if (!hasOpenSSL(3)) { const kKnownUnsupported = ['shake128', 'shake256']; for (const hash of getHashes()) { if (kKnownUnsupported.includes(hash)) continue; diff --git a/test/parallel/test-crypto-hmac.js b/test/parallel/test-crypto-hmac.js index 79abba6952b4..ca6a424c5293 100644 --- a/test/parallel/test-crypto-hmac.js +++ b/test/parallel/test-crypto-hmac.js @@ -6,6 +6,9 @@ if (!common.hasCrypto) { const assert = require('assert'); const crypto = require('crypto'); +const { hasFIPS } = require('../common/crypto'); + +const fips3 = hasFIPS(3); { const Hmac = crypto.Hmac; @@ -24,7 +27,7 @@ assert.throws( // This used to segfault. See: https://github.com/nodejs/node/issues/9819 assert.throws( - () => crypto.createHmac('sha256', 'key').digest({ + () => crypto.createHmac('sha256', '0123456789abcdef').digest({ toString: () => { throw new Error('boom'); }, }), { @@ -40,9 +43,14 @@ assert.throws( }); function testHmac(algo, key, data, expected) { - // FIPS does not support MD5. - if (crypto.getFips() && algo === 'md5') + if (crypto.getFips() === 1 && algo === 'md5') { + if (fips3) { + assert.throws(() => crypto.createHmac(algo, Buffer.alloc(32)), { + code: 'ERR_OSSL_EVP_UNSUPPORTED', + }); + } return; + } if (!Array.isArray(data)) data = [data]; @@ -70,12 +78,13 @@ function testHmac(algo, key, data, expected) { { // Historically, dss1 and DSS1 are SHA-1 aliases. + const key = '0123456789abcdef'; const expected = - crypto.createHmac('sha1', 'key').update('data').digest('hex'); + crypto.createHmac('sha1', key).update('data').digest('hex'); for (const algo of ['dss1', 'DSS1']) { assert.strictEqual( - crypto.createHmac(algo, 'key').update('data').digest('hex'), + crypto.createHmac(algo, key).update('data').digest('hex'), expected); } } @@ -439,9 +448,12 @@ const rfc2202_sha1 = [ for (const { key, data, hmac } of rfc2202_sha1) testHmac('sha1', key, data, hmac); -assert.strictEqual( - crypto.createHmac('sha256', 'w00t').digest('ucs2'), - crypto.createHmac('sha256', 'w00t').digest().toString('ucs2')); +{ + const key = '0123456789abcdef'; + assert.strictEqual( + crypto.createHmac('sha256', key).digest('ucs2'), + crypto.createHmac('sha256', key).digest().toString('ucs2')); +} // Check initialized -> uninitialized state transition after calling digest(). { @@ -480,7 +492,7 @@ assert.strictEqual( { assert.throws( - () => crypto.createHmac('sha7', 'key'), + () => crypto.createHmac('sha7', '0123456789abcdef'), /Invalid digest/); } diff --git a/test/parallel/test-crypto-key-objects-messageport.js b/test/parallel/test-crypto-key-objects-messageport.js index d23fde0d00d7..a379b84185ce 100644 --- a/test/parallel/test-crypto-key-objects-messageport.js +++ b/test/parallel/test-crypto-key-objects-messageport.js @@ -9,6 +9,7 @@ const { generateKeyPairSync, KeyObject, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; const { createContext } = require('vm'); const { @@ -45,7 +46,7 @@ process.env.HAS_STARTED_WORKER = 1; // The main thread generates keys and passes them to worker threads. const secretKey = generateKeySync('aes', { length: 128 }); const { publicKey, privateKey } = generateKeyPairSync('rsa', { - modulusLength: 1024 + modulusLength: hasFIPS(3) ? 2048 : 1024 }); const cryptoKey = await subtle.generateKey( { name: 'AES-CBC', length: 128 }, false, ['encrypt']); diff --git a/test/parallel/test-crypto-key-objects-to-crypto-key.js b/test/parallel/test-crypto-key-objects-to-crypto-key.js index a498df144f33..3f74aaaee434 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -12,7 +12,9 @@ const { randomBytes, generateKeyPairSync, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); const { kSupportedAlgorithms } = require('internal/crypto/util'); +const rejectsXCurves = hasFIPS(3, 5); const hashes = Object.keys(kSupportedAlgorithms.digest).filter((name) => { return name.startsWith('SHA-') || name.startsWith('SHA3-'); @@ -232,6 +234,14 @@ function ecVectors(name, usagesByType) { } function cfrgVectors(name, usagesByType) { + if (rejectsXCurves && name.startsWith('X')) { + assert.throws(() => generateKeyPairSync(name.toLowerCase()), { + name: 'Error', + message: /:digital envelope routines::unsupported$/, + }); + return []; + } + const keyPair = generateKeyPairSync(name.toLowerCase()); return asymmetricVectors(keyPair, name, usagesByType); } @@ -317,9 +327,11 @@ const invalid = { 'HMAC': () => macInvalid( { name: 'HMAC', hash: 'SHA-256' }, 'HmacImportParams.length cannot be 0'), - 'X25519': () => invalidAsymmetricKeyType('X25519', 'Ed25519'), }; +if (!rejectsXCurves) + invalid.X25519 = () => invalidAsymmetricKeyType('X25519', 'Ed25519'); + for (const name of ['AES-CBC', 'AES-CTR', 'AES-GCM', 'AES-OCB']) { if (name in kSupportedAlgorithms.importKey) tests[name] = symmetricVectors(name, ['encrypt', 'decrypt']); @@ -350,9 +362,11 @@ for (const [name, usages, invalidAlgorithm] of [ ]) { if (name in kSupportedAlgorithms.importKey) { tests[name] = cfrgVectors(name, usages); - invalid[name] = () => { - invalidAsymmetricKeyType(name, invalidAlgorithm); - }; + if (!rejectsXCurves) { + invalid[name] = () => { + invalidAsymmetricKeyType(name, invalidAlgorithm); + }; + } } } diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js index cfcd50f12a3e..08b211079258 100644 --- a/test/parallel/test-crypto-key-objects.js +++ b/test/parallel/test-crypto-key-objects.js @@ -24,7 +24,17 @@ const { generateKeyPairSync, } = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { + hasOpenSSL, + hasFIPS, +} = require('../common/crypto'); + +const fips3 = hasFIPS(3); +const fips35 = hasFIPS(3, 5); +const fips30 = fips3 && !fips35; +const fips4 = hasFIPS(4); +const rejectsXCurves = fips35; +const fipsDigestErrorCode = 'ERR_OSSL_DIGEST_NOT_ALLOWED'; const fixtures = require('../common/fixtures'); @@ -201,15 +211,26 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', // It should also be possible to import an encrypted private key as a public // key. + const passphrase = 'password'; + if (fips4) { + assert.throws(() => privateKey.export({ + type: 'pkcs8', + format: 'pem', + passphrase: '123', + cipher: 'aes-128-cbc' + }), { + code: 'ERR_OSSL_PASSWORD_STRENGTH_TOO_WEAK', + }); + } const decryptedKey = createPublicKey({ key: privateKey.export({ type: 'pkcs8', format: 'pem', - passphrase: '123', + passphrase, cipher: 'aes-128-cbc' }), format: 'pem', - passphrase: '123' + passphrase }); assert.strictEqual(decryptedKey.type, 'public'); assert.strictEqual(decryptedKey.asymmetricKeyType, 'rsa'); @@ -322,7 +343,7 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', // This should not cause a crash: https://github.com/nodejs/node/issues/25247 assert.throws(() => { createPrivateKey({ key: '' }); - }, hasOpenSSL3 ? { + }, hasOpenSSL(3) ? { message: 'error:1E08010C:DECODER routines::unsupported', } : process.features.openssl_is_boringssl ? { message: 'error:0900006e:PEM routines:OPENSSL_internal:NO_START_LINE', @@ -354,7 +375,7 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', type: 'pkcs1' }); createPrivateKey({ key, format: 'der', type: 'pkcs1' }); - }, hasOpenSSL3 ? { + }, hasOpenSSL(3) ? { message: /error:1E08010C:DECODER routines::unsupported/, library: 'DECODER routines' } : process.features.openssl_is_boringssl ? { @@ -444,6 +465,8 @@ for (const info of [ } }, ]) { const keyType = info.keyType; + const fipsUnsupported = + rejectsXCurves && keyType.startsWith('x'); if (process.features.openssl_is_boringssl && keyType.endsWith('448')) { common.printSkipMessage(`Skipping unsupported ${keyType} test case`); @@ -462,19 +485,32 @@ for (const info of [ } { - const key = createPrivateKey({ key: info.jwk, format: 'jwk' }); - assert.strictEqual(key.type, 'private'); - assert.strictEqual(key.asymmetricKeyType, keyType); - assert.strictEqual(key.symmetricKeySize, undefined); - assert.strictEqual( - key.export({ type: 'pkcs8', format: 'pem' }), info.private); - assert.deepStrictEqual( - key.export({ format: 'jwk' }), info.jwk); + if (fipsUnsupported) { + assert.throws( + () => createPrivateKey({ key: info.jwk, format: 'jwk' }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + } else { + const key = createPrivateKey({ key: info.jwk, format: 'jwk' }); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'pkcs8', format: 'pem' }), info.private); + assert.deepStrictEqual( + key.export({ format: 'jwk' }), info.jwk); + } } { - for (const input of [ - info.private, info.public, { key: info.jwk, format: 'jwk' }]) { + const inputs = [info.private, info.public]; + if (fipsUnsupported) { + assert.throws( + () => createPublicKey({ key: info.jwk, format: 'jwk' }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + } else { + inputs.push({ key: info.jwk, format: 'jwk' }); + } + for (const input of inputs) { const key = createPublicKey(input); assert.strictEqual(key.type, 'public'); assert.strictEqual(key.asymmetricKeyType, keyType); @@ -498,21 +534,32 @@ for (const info of [ assert(Buffer.isBuffer(rawPriv)); assert(Buffer.isBuffer(rawPub)); - const importedPriv = createPrivateKey({ + const privateOptions = { key: rawPriv, format: 'raw-private', asymmetricKeyType: keyType, - }); - assert.strictEqual(importedPriv.type, 'private'); - assert.strictEqual(importedPriv.asymmetricKeyType, keyType); - assert.deepStrictEqual( - importedPriv.export({ format: 'raw-private' }), rawPriv); - - const importedPub = createPublicKey({ + }; + const publicOptions = { key: rawPub, format: 'raw-public', asymmetricKeyType: keyType, - }); - assert.strictEqual(importedPub.type, 'public'); - assert.strictEqual(importedPub.asymmetricKeyType, keyType); - assert.deepStrictEqual( - importedPub.export({ format: 'raw-public' }), rawPub); + }; + if (fipsUnsupported) { + assert.throws( + () => createPrivateKey(privateOptions), + { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws( + () => createPublicKey(publicOptions), + { code: 'ERR_INVALID_ARG_VALUE' }); + } else { + const importedPriv = createPrivateKey(privateOptions); + assert.strictEqual(importedPriv.type, 'private'); + assert.strictEqual(importedPriv.asymmetricKeyType, keyType); + assert.deepStrictEqual( + importedPriv.export({ format: 'raw-private' }), rawPriv); + + const importedPub = createPublicKey(publicOptions); + assert.strictEqual(importedPub.type, 'public'); + assert.strictEqual(importedPub.asymmetricKeyType, keyType); + assert.deepStrictEqual( + importedPub.export({ format: 'raw-public' }), rawPub); + } } } @@ -602,6 +649,7 @@ for (const info of [ } }, ]) { const { keyType, namedCurve } = info; + const fipsUnsupported = fips3 && namedCurve === 'secp256k1'; if (process.features.openssl_is_boringssl && !getCurves().includes(namedCurve)) { common.printSkipMessage(`Skipping unsupported ${keyType} test case`); @@ -621,20 +669,33 @@ for (const info of [ } { - const key = createPrivateKey({ key: info.jwk, format: 'jwk' }); - assert.strictEqual(key.type, 'private'); - assert.strictEqual(key.asymmetricKeyType, keyType); - assert.deepStrictEqual(key.asymmetricKeyDetails, { namedCurve }); - assert.strictEqual(key.symmetricKeySize, undefined); - assert.strictEqual( - key.export({ type: 'pkcs8', format: 'pem' }), info.private); - assert.deepStrictEqual( - key.export({ format: 'jwk' }), info.jwk); + if (fipsUnsupported) { + assert.throws( + () => createPrivateKey({ key: info.jwk, format: 'jwk' }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + } else { + const key = createPrivateKey({ key: info.jwk, format: 'jwk' }); + assert.strictEqual(key.type, 'private'); + assert.strictEqual(key.asymmetricKeyType, keyType); + assert.deepStrictEqual(key.asymmetricKeyDetails, { namedCurve }); + assert.strictEqual(key.symmetricKeySize, undefined); + assert.strictEqual( + key.export({ type: 'pkcs8', format: 'pem' }), info.private); + assert.deepStrictEqual( + key.export({ format: 'jwk' }), info.jwk); + } } { - for (const input of [ - info.private, info.public, { key: info.jwk, format: 'jwk' }]) { + const inputs = [info.private, info.public]; + if (fipsUnsupported) { + assert.throws( + () => createPublicKey({ key: info.jwk, format: 'jwk' }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + } else { + inputs.push({ key: info.jwk, format: 'jwk' }); + } + for (const input of inputs) { const key = createPublicKey(input); assert.strictEqual(key.type, 'public'); assert.strictEqual(key.asymmetricKeyType, keyType); @@ -669,23 +730,35 @@ for (const info of [ assert(Buffer.isBuffer(rawPriv)); assert(Buffer.isBuffer(rawPub)); - const importedPriv = createPrivateKey({ + const privateOptions = { key: rawPriv, format: 'raw-private', asymmetricKeyType: keyType, namedCurve, - }); - assert.strictEqual(importedPriv.type, 'private'); - assert.strictEqual(importedPriv.asymmetricKeyType, keyType); - assert.deepStrictEqual( - importedPriv.export({ format: 'raw-private' }), rawPriv); + }; - const importedPub = createPublicKey({ + const publicOptions = { key: rawPub, format: 'raw-public', asymmetricKeyType: keyType, namedCurve, - }); - assert.strictEqual(importedPub.type, 'public'); - assert.strictEqual(importedPub.asymmetricKeyType, keyType); - assert.deepStrictEqual( - importedPub.export({ format: 'raw-public' }), rawPub); + }; + if (fipsUnsupported) { + assert.throws( + () => createPrivateKey(privateOptions), + { code: 'ERR_INVALID_ARG_VALUE' }); + assert.throws( + () => createPublicKey(publicOptions), + { code: 'ERR_INVALID_ARG_VALUE' }); + } else { + const importedPriv = createPrivateKey(privateOptions); + assert.strictEqual(importedPriv.type, 'private'); + assert.strictEqual(importedPriv.asymmetricKeyType, keyType); + assert.deepStrictEqual( + importedPriv.export({ format: 'raw-private' }), rawPriv); + + const importedPub = createPublicKey(publicOptions); + assert.strictEqual(importedPub.type, 'public'); + assert.strictEqual(importedPub.asymmetricKeyType, keyType); + assert.deepStrictEqual( + importedPub.export({ format: 'raw-public' }), rawPub); + } } } @@ -728,7 +801,7 @@ for (const info of [ { // Reading an encrypted key without a passphrase should fail. - assert.throws(() => createPrivateKey(privateDsa), hasOpenSSL3 ? { + assert.throws(() => createPrivateKey(privateDsa), hasOpenSSL(3) ? { name: 'Error', message: 'error:07880109:common libcrypto routines::interrupted or ' + 'cancelled', @@ -744,7 +817,7 @@ for (const info of [ key: privateDsa, format: 'pem', passphrase: Buffer.alloc(1025, 'a') - }), hasOpenSSL3 ? { name: 'Error' } : { + }), hasOpenSSL(3) ? { name: 'Error' } : { code: 'ERR_OSSL_PEM_BAD_PASSWORD_READ', name: 'Error' }); @@ -755,8 +828,10 @@ for (const info of [ key: privateDsa, format: 'pem', passphrase: Buffer.alloc(1024, 'a') - }), { - message: /bad decrypt|BAD_DECRYPT/ + }), fips4 ? { + code: 'ERR_OSSL_INVALID_SALT_LENGTH', + } : { + message: /bad decrypt|BAD_DECRYPT/, }); const publicKey = createPublicKey(publicDsa); @@ -767,11 +842,28 @@ for (const info of [ () => publicKey.export({ format: 'jwk' }), { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE' }); - const privateKey = createPrivateKey({ - key: privateDsa, + const privateKeyData = + createPrivateKey(fixtures.readKey('dsa_private.pem')).export({ + type: 'pkcs8', + format: 'pem', + cipher: 'aes-256-cbc', + passphrase: 'password', + }); + const privateKeyOptions = { + key: privateKeyData, format: 'pem', - passphrase: 'secret' - }); + passphrase: 'password', + }; + if (fips4) { + assert.throws(() => createPrivateKey({ + key: privateDsa, + format: 'pem', + passphrase: 'secret', + }), { + code: 'ERR_OSSL_PASSWORD_STRENGTH_TOO_WEAK', + }); + } + const privateKey = createPrivateKey(privateKeyOptions); assert.strictEqual(privateKey.type, 'private'); assert.strictEqual(privateKey.asymmetricKeyType, 'dsa'); assert.strictEqual(privateKey.symmetricKeySize, undefined); @@ -815,20 +907,27 @@ if (!process.features.openssl_is_boringssl) { { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE' }); for (const key of [privatePem, privateKey]) { - // Any algorithm should work. - for (const algo of ['sha1', 'sha256']) { - // Any salt length should work. - for (const saltLength of [undefined, 8, 10, 12, 16, 18, 20]) { - const signature = createSign(algo) - .update('foo') - .sign({ key, saltLength }); - - for (const pkey of [key, publicKey, publicPem]) { - const okay = createVerify(algo) - .update('foo') - .verify({ key: pkey, saltLength }, signature); - - assert.ok(okay); + if (fips30) { + // With no explicit parameters, this key defaults to SHA-1 for PSS. + assert.throws(() => createSign('sha256').update('foo').sign(key), { + code: fipsDigestErrorCode, + }); + } else { + // Any algorithm should work. + for (const algo of ['sha1', 'sha256']) { + // Any salt length should work. + for (const saltLength of [undefined, 8, 10, 12, 16, 18, 20]) { + const signature = createSign(algo) + .update('foo') + .sign({ key, saltLength }); + + for (const pkey of [key, publicKey, publicPem]) { + const okay = createVerify(algo) + .update('foo') + .verify({ key: pkey, saltLength }, signature); + + assert.ok(okay); + } } } } @@ -896,12 +995,15 @@ if (!process.features.openssl_is_boringssl) { // Signing with anything other than sha256 should fail. assert.throws(() => { createSign('sha1').sign(key); - }, /digest not allowed/); + }, fips30 ? { + code: fipsDigestErrorCode, + } : /digest not allowed/); // Signing with salt lengths less than 16 bytes should fail. for (const saltLength of [8, 10, 12]) { assert.throws(() => { - createSign('sha1').sign({ key, saltLength }); + createSign(fips3 ? 'sha256' : 'sha1') + .sign({ key, saltLength }); }, /pss saltlen too small/); } @@ -994,7 +1096,9 @@ if (!process.features.openssl_is_boringssl) { for (const algo of ['sha1', 'sha256']) { assert.throws(() => { createSign(algo).sign(key); - }, /digest not allowed/); + }, fips30 && algo === 'sha1' ? { + code: fipsDigestErrorCode, + } : /digest not allowed/); } // sha512 should produce a valid signature. @@ -1057,20 +1161,27 @@ if (!process.features.openssl_is_boringssl) { // provider is currently in use. const namedCurve = getCurves().find((curve) => !supported.includes(curve)); assert(namedCurve); - const keyPair = generateKeyPairSync('ec', { namedCurve }); - const { publicKey, privateKey } = keyPair; - assert.throws( - () => publicKey.export({ format: 'jwk' }), - { - code: 'ERR_CRYPTO_JWK_UNSUPPORTED_CURVE', - message: `Unsupported JWK EC curve: ${namedCurve}.` - }); - assert.throws( - () => privateKey.export({ format: 'jwk' }), - { - code: 'ERR_CRYPTO_JWK_UNSUPPORTED_CURVE', - message: `Unsupported JWK EC curve: ${namedCurve}.` + if (fips3) { + assert.throws(() => generateKeyPairSync('ec', { namedCurve }), { + name: 'Error', + message: 'error:08000081:elliptic curve routines::unknown group', }); + } else { + const keyPair = generateKeyPairSync('ec', { namedCurve }); + const { publicKey, privateKey } = keyPair; + assert.throws( + () => publicKey.export({ format: 'jwk' }), + { + code: 'ERR_CRYPTO_JWK_UNSUPPORTED_CURVE', + message: `Unsupported JWK EC curve: ${namedCurve}.` + }); + assert.throws( + () => privateKey.export({ format: 'jwk' }), + { + code: 'ERR_CRYPTO_JWK_UNSUPPORTED_CURVE', + message: `Unsupported JWK EC curve: ${namedCurve}.` + }); + } } { @@ -1113,12 +1224,19 @@ if (!process.features.openssl_is_boringssl) { { const first = generateKeyPairSync('ed25519'); - const second = generateKeyPairSync('x25519'); + if (rejectsXCurves) { + assert.throws(() => generateKeyPairSync('x25519'), { + name: 'Error', + message: /:digital envelope routines::unsupported$/, + }); + } else { + const second = generateKeyPairSync('x25519'); - assert(!first.publicKey.equals(second.publicKey)); - assert(!first.publicKey.equals(second.privateKey)); - assert(!first.privateKey.equals(second.privateKey)); - assert(!first.privateKey.equals(second.publicKey)); + assert(!first.publicKey.equals(second.publicKey)); + assert(!first.publicKey.equals(second.privateKey)); + assert(!first.privateKey.equals(second.privateKey)); + assert(!first.privateKey.equals(second.publicKey)); + } } { diff --git a/test/parallel/test-crypto-key-store.js b/test/parallel/test-crypto-key-store.js index b6f012416673..e777440f2c0d 100644 --- a/test/parallel/test-crypto-key-store.js +++ b/test/parallel/test-crypto-key-store.js @@ -2,7 +2,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3)) common.skip('requires OpenSSL 3.x'); @@ -84,26 +84,33 @@ const data = Buffer.from('hello store'); } { - const alice = generateKeyPairSync('x25519'); - const bob = generateKeyPairSync('x25519'); - const file = path.join(tmpdir.path, 'x25519.pem'); - fs.writeFileSync(file, alice.privateKey.export({ - format: 'pem', - type: 'pkcs8', - })); - const url = pathToFileURL(file); + if (hasFIPS(3, 5)) { + assert.throws(() => generateKeyPairSync('x25519'), { + name: 'Error', + message: /:digital envelope routines::unsupported$/, + }); + } else { + const alice = generateKeyPairSync('x25519'); + const bob = generateKeyPairSync('x25519'); + const file = path.join(tmpdir.path, 'x25519.pem'); + fs.writeFileSync(file, alice.privateKey.export({ + format: 'pem', + type: 'pkcs8', + })); + const url = pathToFileURL(file); - const expected = diffieHellman({ - privateKey: alice.privateKey, - publicKey: bob.publicKey, - }); - assert.deepStrictEqual( - diffieHellman({ privateKey: url, publicKey: bob.publicKey }), - expected); + const expected = diffieHellman({ + privateKey: alice.privateKey, + publicKey: bob.publicKey, + }); + assert.deepStrictEqual( + diffieHellman({ privateKey: url, publicKey: bob.publicKey }), + expected); - if (hasOpenSSL(3, 2)) { - const { sharedKey, ciphertext } = encapsulate(alice.publicKey); - assert.deepStrictEqual(decapsulate(url, ciphertext), sharedKey); + if (hasOpenSSL(3, 2)) { + const { sharedKey, ciphertext } = encapsulate(alice.publicKey); + assert.deepStrictEqual(decapsulate(url, ciphertext), sharedKey); + } } } diff --git a/test/parallel/test-crypto-keygen-async-dsa.js b/test/parallel/test-crypto-keygen-async-dsa.js index d7c857d35e21..6c7129c7efba 100644 --- a/test/parallel/test-crypto-keygen-async-dsa.js +++ b/test/parallel/test-crypto-keygen-async-dsa.js @@ -17,7 +17,7 @@ const { spkiExp, } = require('../common/crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); // Test async DSA key generation. { @@ -27,7 +27,7 @@ const { hasOpenSSL3 } = require('../common/crypto'); }; generateKeyPair('dsa', { - modulusLength: hasOpenSSL3 ? 2048 : 512, + modulusLength: hasOpenSSL(3) ? 2048 : 512, divisorLength: 256, publicKeyEncoding: { type: 'spki', @@ -35,7 +35,7 @@ const { hasOpenSSL3 } = require('../common/crypto'); }, privateKeyEncoding: { cipher: 'aes-128-cbc', - passphrase: 'secret', + passphrase: 'password', ...privateKeyEncoding } }, common.mustSucceed((publicKey, privateKeyDER) => { @@ -44,8 +44,8 @@ const { hasOpenSSL3 } = require('../common/crypto'); // The private key is DER-encoded. assert(Buffer.isBuffer(privateKeyDER)); - assertApproximateSize(publicKey, hasOpenSSL3 ? 1194 : 440); - assertApproximateSize(privateKeyDER, hasOpenSSL3 ? 721 : 336); + assertApproximateSize(publicKey, hasOpenSSL(3) ? 1194 : 440); + assertApproximateSize(privateKeyDER, hasOpenSSL(3) ? 721 : 336); // Since the private key is encrypted, signing shouldn't work anymore. assert.throws(() => { @@ -63,7 +63,7 @@ const { hasOpenSSL3 } = require('../common/crypto'); testSignVerify(publicKey, { key: privateKeyDER, ...privateKeyEncoding, - passphrase: 'secret' + passphrase: 'password' }); })); } diff --git a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-ec.js b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-ec.js index b0945dcc83a2..8467c7b97aa9 100644 --- a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-ec.js +++ b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-ec.js @@ -8,6 +8,7 @@ const assert = require('assert'); const { generateKeyPair, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); // Test async elliptic curve key generation with 'jwk' encoding and named // curve. @@ -24,7 +25,12 @@ for (const curve of ['P-384', 'P-256', 'P-521', 'secp256k1']) { privateKeyEncoding: { format: 'jwk' } - }, common.mustSucceed((publicKey, privateKey) => { + }, common.mustCall((err, publicKey, privateKey) => { + if (hasFIPS(3) && curve === 'secp256k1') { + assert.strictEqual(err?.message, 'error:08000081:elliptic curve routines::unknown group'); + return; + } + assert.ifError(err); assert.strictEqual(typeof publicKey, 'object'); assert.strictEqual(typeof privateKey, 'object'); assert.strictEqual(publicKey.x, privateKey.x); diff --git a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-rsa.js b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-rsa.js index 449d1a97f9f6..f1a1bdf8322f 100644 --- a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-rsa.js +++ b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-rsa.js @@ -8,11 +8,12 @@ const assert = require('assert'); const { generateKeyPair, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); // Test async elliptic curve key generation with 'jwk' encoding and RSA. { generateKeyPair('rsa', { - modulusLength: 1024, + modulusLength: hasFIPS(3) ? 2048 : 1024, publicKeyEncoding: { format: 'jwk' }, diff --git a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk.js b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk.js index 731960b0d56a..4839ee4fc91d 100644 --- a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk.js +++ b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk.js @@ -8,6 +8,8 @@ const assert = require('assert'); const { generateKeyPair, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); +const rejectsXCurves = hasFIPS(3, 5); // Test async elliptic curve key generation with 'jwk' encoding. { @@ -23,7 +25,12 @@ const { privateKeyEncoding: { format: 'jwk' } - }, common.mustSucceed((publicKey, privateKey) => { + }, common.mustCall((err, publicKey, privateKey) => { + if (rejectsXCurves && type.startsWith('x')) { + assert.strictEqual(err?.message, 'error:0308010C:digital envelope routines::unsupported'); + return; + } + assert.ifError(err); assert.strictEqual(typeof publicKey, 'object'); assert.strictEqual(typeof privateKey, 'object'); assert.strictEqual(publicKey.x, privateKey.x); diff --git a/test/parallel/test-crypto-keygen-async-encrypted-private-key-der.js b/test/parallel/test-crypto-keygen-async-encrypted-private-key-der.js index 3203dfe16eb6..c1bc6fc95ea0 100644 --- a/test/parallel/test-crypto-keygen-async-encrypted-private-key-der.js +++ b/test/parallel/test-crypto-keygen-async-encrypted-private-key-der.js @@ -10,15 +10,17 @@ const { } = require('crypto'); const { assertApproximateSize, + hasFIPS, testEncryptDecrypt, testSignVerify, } = require('../common/crypto'); // Test async RSA key generation with an encrypted private key, but encoded as DER. { + const isFips = hasFIPS(3); generateKeyPair('rsa', { publicExponent: 0x10001, - modulusLength: 512, + modulusLength: isFips ? 2048 : 512, publicKeyEncoding: { type: 'pkcs1', format: 'der' @@ -29,7 +31,7 @@ const { } }, common.mustSucceed((publicKeyDER, privateKeyDER) => { assert(Buffer.isBuffer(publicKeyDER)); - assertApproximateSize(publicKeyDER, 74); + assertApproximateSize(publicKeyDER, isFips ? 270 : 74); assert(Buffer.isBuffer(privateKeyDER)); diff --git a/test/parallel/test-crypto-keygen-async-encrypted-private-key.js b/test/parallel/test-crypto-keygen-async-encrypted-private-key.js index 727cccc6f3ef..37a0121742df 100644 --- a/test/parallel/test-crypto-keygen-async-encrypted-private-key.js +++ b/test/parallel/test-crypto-keygen-async-encrypted-private-key.js @@ -10,15 +10,18 @@ const { } = require('crypto'); const { assertApproximateSize, + hasFIPS, testEncryptDecrypt, testSignVerify, } = require('../common/crypto'); // Test async RSA key generation with an encrypted private key, but encoded as DER. { + const isFips = hasFIPS(3); + const passphrase = 'password'; generateKeyPair('rsa', { publicExponent: 0x10001, - modulusLength: 512, + modulusLength: isFips ? 2048 : 512, publicKeyEncoding: { type: 'pkcs1', format: 'der' @@ -27,11 +30,11 @@ const { type: 'pkcs8', format: 'der', cipher: 'aes-256-cbc', - passphrase: 'secret' + passphrase } }, common.mustSucceed((publicKeyDER, privateKeyDER) => { assert(Buffer.isBuffer(publicKeyDER)); - assertApproximateSize(publicKeyDER, 74); + assertApproximateSize(publicKeyDER, isFips ? 270 : 74); assert(Buffer.isBuffer(privateKeyDER)); @@ -59,7 +62,7 @@ const { key: privateKeyDER, format: 'der', type: 'pkcs8', - passphrase: 'secret' + passphrase }; testEncryptDecrypt(publicKey, privateKey); testSignVerify(publicKey, privateKey); diff --git a/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js b/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js index c3b8ab6e8f50..081e709f46ec 100644 --- a/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js +++ b/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js @@ -12,10 +12,11 @@ const { generateKeyPair, } = require('crypto'); const { + hasFIPS, testSignVerify, spkiExp, sec1EncExp, - hasOpenSSL3, + hasOpenSSL, } = require('../common/crypto'); { @@ -34,7 +35,12 @@ const { cipher: 'aes-128-cbc', passphrase: 'secret' } - }, common.mustSucceed((publicKey, privateKey) => { + }, common.mustCall((err, publicKey, privateKey) => { + if (hasFIPS(3)) { + assert.strictEqual(err?.code, 'ERR_OSSL_EVP_UNSUPPORTED'); + return; + } + assert.ifError(err); assert.strictEqual(typeof publicKey, 'string'); assert.match(publicKey, spkiExp); assert.strictEqual(typeof privateKey, 'string'); @@ -42,7 +48,7 @@ const { // Since the private key is encrypted, signing shouldn't work anymore. assert.throws(() => testSignVerify(publicKey, privateKey), - hasOpenSSL3 ? { + hasOpenSSL(3) ? { message: 'error:07880109:common libcrypto ' + 'routines::interrupted or cancelled' } : { diff --git a/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted.js b/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted.js index 0503ff74787f..84ea9d2f7a9a 100644 --- a/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted.js +++ b/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted.js @@ -9,10 +9,11 @@ const { generateKeyPair, } = require('crypto'); const { + hasFIPS, testSignVerify, spkiExp, sec1EncExp, - hasOpenSSL3, + hasOpenSSL, } = require('../common/crypto'); { @@ -31,7 +32,12 @@ const { cipher: 'aes-128-cbc', passphrase: 'secret' } - }, common.mustSucceed((publicKey, privateKey) => { + }, common.mustCall((err, publicKey, privateKey) => { + if (hasFIPS(3)) { + assert.strictEqual(err?.code, 'ERR_OSSL_EVP_UNSUPPORTED'); + return; + } + assert.ifError(err); assert.strictEqual(typeof publicKey, 'string'); assert.match(publicKey, spkiExp); assert.strictEqual(typeof privateKey, 'string'); @@ -39,7 +45,7 @@ const { // Since the private key is encrypted, signing shouldn't work anymore. assert.throws(() => testSignVerify(publicKey, privateKey), - hasOpenSSL3 ? { + hasOpenSSL(3) ? { message: 'error:07880109:common libcrypto ' + 'routines::interrupted or cancelled' } : { diff --git a/test/parallel/test-crypto-keygen-async-rsa.js b/test/parallel/test-crypto-keygen-async-rsa.js index c80d7d334929..7a372ded9fc5 100644 --- a/test/parallel/test-crypto-keygen-async-rsa.js +++ b/test/parallel/test-crypto-keygen-async-rsa.js @@ -10,17 +10,19 @@ const { } = require('crypto'); const { assertApproximateSize, + hasFIPS, testEncryptDecrypt, testSignVerify, pkcs1EncExp, - hasOpenSSL3, + hasOpenSSL, } = require('../common/crypto'); // Test async RSA key generation with an encrypted private key. { + const isFips = hasFIPS(3); generateKeyPair('rsa', { publicExponent: 0x10001, - modulusLength: 512, + modulusLength: isFips ? 2048 : 512, publicKeyEncoding: { type: 'pkcs1', format: 'der' @@ -31,7 +33,12 @@ const { cipher: 'aes-256-cbc', passphrase: 'secret' } - }, common.mustSucceed((publicKeyDER, privateKey) => { + }, common.mustCall((err, publicKeyDER, privateKey) => { + if (isFips) { + assert.strictEqual(err?.code, 'ERR_OSSL_EVP_UNSUPPORTED'); + return; + } + assert.ifError(err); assert(Buffer.isBuffer(publicKeyDER)); assertApproximateSize(publicKeyDER, 74); @@ -44,7 +51,7 @@ const { type: 'pkcs1', format: 'der', }; - const expectedError = hasOpenSSL3 ? { + const expectedError = hasOpenSSL(3) ? { name: 'Error', message: 'error:07880109:common libcrypto routines::interrupted or ' + 'cancelled' diff --git a/test/parallel/test-crypto-keygen-bit-length.js b/test/parallel/test-crypto-keygen-bit-length.js index 13234589a5d6..cf3bb094e50f 100644 --- a/test/parallel/test-crypto-keygen-bit-length.js +++ b/test/parallel/test-crypto-keygen-bit-length.js @@ -12,7 +12,9 @@ const assert = require('assert'); const { generateKeyPair, } = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); + +const fips3 = hasFIPS(3); // This tests check that generateKeyPair returns correct bit length in // KeyObject's asymmetricKeyDetails. @@ -20,23 +22,38 @@ const { hasOpenSSL3 } = require('../common/crypto'); { generateKeyPair('rsa', { modulusLength: 513, - }, common.mustSucceed((publicKey, privateKey) => { + }, common.mustCall((err, publicKey, privateKey) => { + if (fips3) { + assert.strictEqual(err?.message, 'error:020000AE:rsa routines::invalid modulus'); + return; + } + assert.ifError(err); assert.strictEqual(privateKey.asymmetricKeyDetails.modulusLength, 513); assert.strictEqual(publicKey.asymmetricKeyDetails.modulusLength, 513); })); generateKeyPair('rsa-pss', { modulusLength: 513, - }, common.mustSucceed((publicKey, privateKey) => { + }, common.mustCall((err, publicKey, privateKey) => { + if (fips3) { + assert.strictEqual(err?.message, 'error:020000AE:rsa routines::invalid modulus'); + return; + } + assert.ifError(err); assert.strictEqual(privateKey.asymmetricKeyDetails.modulusLength, 513); assert.strictEqual(publicKey.asymmetricKeyDetails.modulusLength, 513); })); - if (hasOpenSSL3) { + if (hasOpenSSL(3)) { generateKeyPair('dsa', { modulusLength: 2049, divisorLength: 256, - }, common.mustSucceed((publicKey, privateKey) => { + }, common.mustCall((err, publicKey, privateKey) => { + if (fips3) { + assert.strictEqual(err?.message, 'error:05000072:dsa routines::bad ffc parameters'); + return; + } + assert.ifError(err); assert.strictEqual(privateKey.asymmetricKeyDetails.modulusLength, 2049); assert.strictEqual(publicKey.asymmetricKeyDetails.modulusLength, 2049); })); diff --git a/test/parallel/test-crypto-keygen-deprecation.js b/test/parallel/test-crypto-keygen-deprecation.js index 926dfbbc4ae9..62266382e2da 100644 --- a/test/parallel/test-crypto-keygen-deprecation.js +++ b/test/parallel/test-crypto-keygen-deprecation.js @@ -15,13 +15,15 @@ DeprecationWarning.push([ common.expectWarning({ DeprecationWarning }); const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); +const modulusLength = hasFIPS(3) ? 2048 : 512; const { generateKeyPair } = require('crypto'); { // This test makes sure deprecated options still work as intended generateKeyPair('rsa-pss', { - modulusLength: 512, + modulusLength, saltLength: 16, hash: 'sha256', mgf1Hash: 'sha256' @@ -29,7 +31,7 @@ const { generateKeyPair } = require('crypto'); assert.strictEqual(publicKey.type, 'public'); assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss'); assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', @@ -39,7 +41,7 @@ const { generateKeyPair } = require('crypto'); assert.strictEqual(privateKey.type, 'private'); assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', diff --git a/test/parallel/test-crypto-keygen-dh-classic.js b/test/parallel/test-crypto-keygen-dh-classic.js index 44af7730126a..13c5db2d0603 100644 --- a/test/parallel/test-crypto-keygen-dh-classic.js +++ b/test/parallel/test-crypto-keygen-dh-classic.js @@ -11,11 +11,12 @@ const assert = require('assert'); const { generateKeyPair, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); // Test classic Diffie-Hellman key generation. { generateKeyPair('dh', { - primeLength: 512 + primeLength: hasFIPS(3) ? 2048 : 512 }, common.mustSucceed((publicKey, privateKey) => { assert.strictEqual(publicKey.type, 'public'); assert.strictEqual(publicKey.asymmetricKeyType, 'dh'); diff --git a/test/parallel/test-crypto-keygen-duplicate-deprecated-option.js b/test/parallel/test-crypto-keygen-duplicate-deprecated-option.js index 854ad6e35efc..4f7ffcfce061 100644 --- a/test/parallel/test-crypto-keygen-duplicate-deprecated-option.js +++ b/test/parallel/test-crypto-keygen-duplicate-deprecated-option.js @@ -5,6 +5,8 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); +const modulusLength = hasFIPS(3) ? 2048 : 512; const { generateKeyPair, } = require('crypto'); @@ -13,7 +15,7 @@ const { // simultaneously so long as they're identical values. { generateKeyPair('rsa-pss', { - modulusLength: 512, + modulusLength, saltLength: 16, hash: 'sha256', hashAlgorithm: 'sha256', @@ -23,7 +25,7 @@ const { assert.strictEqual(publicKey.type, 'public'); assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss'); assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', @@ -33,7 +35,7 @@ const { assert.strictEqual(privateKey.type, 'private'); assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', diff --git a/test/parallel/test-crypto-keygen-eddsa.js b/test/parallel/test-crypto-keygen-eddsa.js index 0a132235ea28..6e4e5c71ecbc 100644 --- a/test/parallel/test-crypto-keygen-eddsa.js +++ b/test/parallel/test-crypto-keygen-eddsa.js @@ -8,6 +8,8 @@ const assert = require('assert'); const { generateKeyPair, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); +const rejectsXCurves = hasFIPS(3, 5); // Test EdDSA key generation. { @@ -16,7 +18,12 @@ const { common.printSkipMessage(`Skipping unsupported ${keyType} test case`); continue; } - generateKeyPair(keyType, common.mustSucceed((publicKey, privateKey) => { + generateKeyPair(keyType, common.mustCall((err, publicKey, privateKey) => { + if (rejectsXCurves && keyType.startsWith('x')) { + assert.strictEqual(err?.message, 'error:0308010C:digital envelope routines::unsupported'); + return; + } + assert.ifError(err); assert.strictEqual(publicKey.type, 'public'); assert.strictEqual(publicKey.asymmetricKeyType, keyType); assert.deepStrictEqual(publicKey.asymmetricKeyDetails, {}); diff --git a/test/parallel/test-crypto-keygen-empty-passphrase-no-error.js b/test/parallel/test-crypto-keygen-empty-passphrase-no-error.js index 6c7938f99e1b..ae1ca23c29e8 100644 --- a/test/parallel/test-crypto-keygen-empty-passphrase-no-error.js +++ b/test/parallel/test-crypto-keygen-empty-passphrase-no-error.js @@ -8,11 +8,14 @@ const assert = require('assert'); const { generateKeyPair, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); + +const fips4 = hasFIPS(4); // Passing an empty passphrase string should not throw ERR_OSSL_CRYPTO_MALLOC_FAILURE even on OpenSSL 3. // Regression test for https://github.com/nodejs/node/issues/41428. generateKeyPair('rsa', { - modulusLength: 1024, + modulusLength: hasFIPS(3) ? 2048 : 1024, publicKeyEncoding: { type: 'spki', format: 'pem' @@ -23,7 +26,12 @@ generateKeyPair('rsa', { cipher: 'aes-256-cbc', passphrase: '' } -}, common.mustSucceed((publicKey, privateKey) => { +}, common.mustCall((err, publicKey, privateKey) => { + if (fips4) { + assert.strictEqual(err?.code, 'ERR_OSSL_PASSWORD_STRENGTH_TOO_WEAK'); + return; + } + assert.ifError(err); assert.strictEqual(typeof publicKey, 'string'); assert.strictEqual(typeof privateKey, 'string'); })); diff --git a/test/parallel/test-crypto-keygen-empty-passphrase-no-prompt.js b/test/parallel/test-crypto-keygen-empty-passphrase-no-prompt.js index cb873ff04748..ccf98bcd3766 100644 --- a/test/parallel/test-crypto-keygen-empty-passphrase-no-prompt.js +++ b/test/parallel/test-crypto-keygen-empty-passphrase-no-prompt.js @@ -10,23 +10,38 @@ const { generateKeyPair, } = require('crypto'); const { + hasFIPS, testSignVerify, - hasOpenSSL3, + hasOpenSSL, } = require('../common/crypto'); +const fips4 = hasFIPS(4); + // Passing an empty passphrase string should not cause OpenSSL's default // passphrase prompt in the terminal. // See https://github.com/nodejs/node/issues/35898. for (const type of ['pkcs1', 'pkcs8']) { generateKeyPair('rsa', { - modulusLength: 1024, + modulusLength: hasFIPS(3) ? 2048 : 1024, privateKeyEncoding: { type, format: 'pem', cipher: 'aes-256-cbc', passphrase: '' } - }, common.mustSucceed((publicKey, privateKey) => { + }, common.mustCall((err, publicKey, privateKey) => { + if (hasFIPS(3) && type === 'pkcs1') { + assert.strictEqual(err?.code, 'ERR_OSSL_EVP_UNSUPPORTED'); + return; + } + if (fips4) { + assert.strictEqual( + err?.code, + 'ERR_OSSL_PASSWORD_STRENGTH_TOO_WEAK', + ); + return; + } + assert.ifError(err); assert.strictEqual(publicKey.type, 'public'); for (const passphrase of ['', Buffer.alloc(0)]) { @@ -41,7 +56,7 @@ for (const type of ['pkcs1', 'pkcs8']) { // the key, and not specifying a passphrase should fail when decoding it. assert.throws(() => { return testSignVerify(publicKey, privateKey); - }, hasOpenSSL3 ? { + }, hasOpenSSL(3) ? { name: 'Error', code: 'ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED', message: 'error:07880109:common libcrypto routines::interrupted or cancelled' diff --git a/test/parallel/test-crypto-keygen-invalid-parameter-encoding-dsa.js b/test/parallel/test-crypto-keygen-invalid-parameter-encoding-dsa.js index 9086e2e8a5f1..930739abd5f1 100644 --- a/test/parallel/test-crypto-keygen-invalid-parameter-encoding-dsa.js +++ b/test/parallel/test-crypto-keygen-invalid-parameter-encoding-dsa.js @@ -12,6 +12,9 @@ const assert = require('assert'); const { generateKeyPairSync, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); + +const fips3 = hasFIPS(3); // Test invalid parameter encoding. { @@ -25,7 +28,11 @@ const { } }), { name: 'Error', - code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE', - message: 'Unsupported JWK Key Type.' + ...fips3 ? { + message: 'error:05000072:dsa routines::bad ffc parameters', + } : { + code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE', + message: 'Unsupported JWK Key Type.', + }, }); } diff --git a/test/parallel/test-crypto-keygen-key-object-without-encoding.js b/test/parallel/test-crypto-keygen-key-object-without-encoding.js index abcd282871b6..3c44d174df6f 100644 --- a/test/parallel/test-crypto-keygen-key-object-without-encoding.js +++ b/test/parallel/test-crypto-keygen-key-object-without-encoding.js @@ -9,6 +9,7 @@ const { generateKeyPair, } = require('crypto'); const { + hasFIPS, testEncryptDecrypt, testSignVerify, } = require('../common/crypto'); @@ -17,7 +18,7 @@ const { { // If no publicKeyEncoding is specified, a key object should be returned. generateKeyPair('rsa', { - modulusLength: 1024, + modulusLength: hasFIPS(3) ? 2048 : 1024, privateKeyEncoding: { type: 'pkcs1', format: 'pem' @@ -36,7 +37,7 @@ const { // If no privateKeyEncoding is specified, a key object should be returned. generateKeyPair('rsa', { - modulusLength: 1024, + modulusLength: hasFIPS(3) ? 2048 : 1024, publicKeyEncoding: { type: 'pkcs1', format: 'pem' diff --git a/test/parallel/test-crypto-keygen-key-objects.js b/test/parallel/test-crypto-keygen-key-objects.js index a0f1bdf2bcb5..2d22e99360e3 100644 --- a/test/parallel/test-crypto-keygen-key-objects.js +++ b/test/parallel/test-crypto-keygen-key-objects.js @@ -8,18 +8,20 @@ const assert = require('assert'); const { generateKeyPairSync, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); // Test sync key generation with key objects. { + const modulusLength = hasFIPS(3) ? 2048 : 512; const { publicKey, privateKey } = generateKeyPairSync('rsa', { - modulusLength: 512 + modulusLength }); assert.strictEqual(typeof publicKey, 'object'); assert.strictEqual(publicKey.type, 'public'); assert.strictEqual(publicKey.asymmetricKeyType, 'rsa'); assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { - modulusLength: 512, + modulusLength, publicExponent: 65537n }); @@ -27,7 +29,7 @@ const { assert.strictEqual(privateKey.type, 'private'); assert.strictEqual(privateKey.asymmetricKeyType, 'rsa'); assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { - modulusLength: 512, + modulusLength, publicExponent: 65537n }); } diff --git a/test/parallel/test-crypto-keygen-missing-oid.js b/test/parallel/test-crypto-keygen-missing-oid.js index 1e4f309292eb..78b70ee50304 100644 --- a/test/parallel/test-crypto-keygen-missing-oid.js +++ b/test/parallel/test-crypto-keygen-missing-oid.js @@ -11,7 +11,7 @@ const { getCurves, } = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); // This test creates EC key pairs on curves without associated OIDs. // Specifying a key encoding should not crash. @@ -21,8 +21,12 @@ const { hasOpenSSL3 } = require('../common/crypto'); if (!getCurves().includes(namedCurve)) continue; - const expectedErrorCode = - hasOpenSSL3 ? 'ERR_OSSL_MISSING_OID' : 'ERR_OSSL_EC_MISSING_OID'; + const expectedError = hasFIPS(3) ? { + name: 'Error', + message: 'error:08000081:elliptic curve routines::unknown group', + } : { + code: hasOpenSSL(3) ? 'ERR_OSSL_MISSING_OID' : 'ERR_OSSL_EC_MISSING_OID', + }; const params = { namedCurve, publicKeyEncoding: { @@ -33,13 +37,9 @@ const { hasOpenSSL3 } = require('../common/crypto'); assert.throws(() => { generateKeyPairSync('ec', params); - }, { - code: expectedErrorCode - }); + }, expectedError); - generateKeyPair('ec', params, common.mustCall((err) => { - assert.strictEqual(err.code, expectedErrorCode); - })); + generateKeyPair('ec', params, common.expectsError(expectedError)); } } } diff --git a/test/parallel/test-crypto-keygen-no-rsassa-pss-params.js b/test/parallel/test-crypto-keygen-no-rsassa-pss-params.js index 559c6f0af051..778899972863 100644 --- a/test/parallel/test-crypto-keygen-no-rsassa-pss-params.js +++ b/test/parallel/test-crypto-keygen-no-rsassa-pss-params.js @@ -11,15 +11,17 @@ const assert = require('assert'); const { generateKeyPair, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); // 'rsa-pss' should not add a RSASSA-PSS-params sequence by default. // Regression test for: https://github.com/nodejs/node/issues/39936 { + const modulusLength = hasFIPS(3) ? 2048 : 512; generateKeyPair('rsa-pss', { - modulusLength: 512 + modulusLength }, common.mustSucceed((publicKey, privateKey) => { const expectedKeyDetails = { - modulusLength: 512, + modulusLength, publicExponent: 65537n }; assert.deepStrictEqual(publicKey.asymmetricKeyDetails, expectedKeyDetails); @@ -30,6 +32,7 @@ const { // AlgorithmIdentifier member of the SubjectPublicKeyInfo has the expected // length of 11 bytes (as opposed to > 11 bytes if node added params). const spki = publicKey.export({ format: 'der', type: 'spki' }); - assert.strictEqual(spki[3], 11, spki.toString('hex')); + assert.strictEqual( + spki[3], hasFIPS(3) ? 32 : 11, spki.toString('hex')); })); } diff --git a/test/parallel/test-crypto-keygen-non-standard-public-exponent.js b/test/parallel/test-crypto-keygen-non-standard-public-exponent.js index f54a9e8a6d9b..96feb6bf8d17 100644 --- a/test/parallel/test-crypto-keygen-non-standard-public-exponent.js +++ b/test/parallel/test-crypto-keygen-non-standard-public-exponent.js @@ -8,28 +8,39 @@ const assert = require('assert'); const { generateKeyPairSync, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); // Test sync key generation with key objects with a non-standard // publicExponent { - const { publicKey, privateKey } = generateKeyPairSync('rsa', { - publicExponent: 3, - modulusLength: 512 - }); + if (hasFIPS(3)) { + assert.throws(() => generateKeyPairSync('rsa', { + publicExponent: 3, + modulusLength: 2048, + }), { + name: 'Error', + message: 'error:020000B2:rsa routines::pub exponent out of range', + }); + } else { + const { publicKey, privateKey } = generateKeyPairSync('rsa', { + publicExponent: 3, + modulusLength: 512 + }); - assert.strictEqual(typeof publicKey, 'object'); - assert.strictEqual(publicKey.type, 'public'); - assert.strictEqual(publicKey.asymmetricKeyType, 'rsa'); - assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { - modulusLength: 512, - publicExponent: 3n - }); + assert.strictEqual(typeof publicKey, 'object'); + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa'); + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { + modulusLength: 512, + publicExponent: 3n + }); - assert.strictEqual(typeof privateKey, 'object'); - assert.strictEqual(privateKey.type, 'private'); - assert.strictEqual(privateKey.asymmetricKeyType, 'rsa'); - assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { - modulusLength: 512, - publicExponent: 3n - }); + assert.strictEqual(typeof privateKey, 'object'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, 'rsa'); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { + modulusLength: 512, + publicExponent: 3n + }); + } } diff --git a/test/parallel/test-crypto-keygen-promisify.js b/test/parallel/test-crypto-keygen-promisify.js index cd6ca7d6e3e6..158367cef46b 100644 --- a/test/parallel/test-crypto-keygen-promisify.js +++ b/test/parallel/test-crypto-keygen-promisify.js @@ -10,6 +10,7 @@ const { } = require('crypto'); const { assertApproximateSize, + hasFIPS, testEncryptDecrypt, testSignVerify, pkcs1PubExp, @@ -19,9 +20,10 @@ const { promisify } = require('util'); // Test the util.promisified API with async RSA key generation. { + const isFips = hasFIPS(3); promisify(generateKeyPair)('rsa', { publicExponent: 0x10001, - modulusLength: 512, + modulusLength: isFips ? 2048 : 512, publicKeyEncoding: { type: 'pkcs1', format: 'pem' @@ -34,11 +36,11 @@ const { promisify } = require('util'); const { publicKey, privateKey } = keys; assert.strictEqual(typeof publicKey, 'string'); assert.match(publicKey, pkcs1PubExp); - assertApproximateSize(publicKey, 180); + assertApproximateSize(publicKey, isFips ? 426 : 180); assert.strictEqual(typeof privateKey, 'string'); assert.match(privateKey, pkcs1PrivExp); - assertApproximateSize(privateKey, 512); + assertApproximateSize(privateKey, isFips ? 1675 : 512); testEncryptDecrypt(publicKey, privateKey); testSignVerify(publicKey, privateKey); diff --git a/test/parallel/test-crypto-keygen-raw.js b/test/parallel/test-crypto-keygen-raw.js index e55c3f10eed8..127ae3762bd1 100644 --- a/test/parallel/test-crypto-keygen-raw.js +++ b/test/parallel/test-crypto-keygen-raw.js @@ -11,7 +11,9 @@ const { createPublicKey, createPrivateKey, } = require('crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); + +const rejectsXCurves = hasFIPS(3, 5); // Test generateKeyPairSync with raw encoding for EdDSA/ECDH key types. { @@ -20,10 +22,18 @@ const { hasOpenSSL } = require('../common/crypto'); types.push('ed448', 'x448'); } for (const type of types) { - const { publicKey, privateKey } = generateKeyPairSync(type, { + const options = { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-private' }, - }); + }; + if (rejectsXCurves && type.startsWith('x')) { + assert.throws(() => generateKeyPairSync(type, options), { + name: 'Error', + message: 'error:0308010C:digital envelope routines::unsupported', + }); + continue; + } + const { publicKey, privateKey } = generateKeyPairSync(type, options); assert(Buffer.isBuffer(publicKey)); assert(Buffer.isBuffer(privateKey)); @@ -54,13 +64,20 @@ const { hasOpenSSL } = require('../common/crypto'); types.push('ed448', 'x448'); } for (const type of types) { - generateKeyPair(type, { + const options = { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-private' }, - }, common.mustSucceed((publicKey, privateKey) => { - assert(Buffer.isBuffer(publicKey)); - assert(Buffer.isBuffer(privateKey)); - })); + }; + generateKeyPair(type, options, + common.mustCall((err, publicKey, privateKey) => { + if (rejectsXCurves && type.startsWith('x')) { + assert.strictEqual(err?.message, 'error:0308010C:digital envelope routines::unsupported'); + return; + } + assert.ifError(err); + assert(Buffer.isBuffer(publicKey)); + assert(Buffer.isBuffer(privateKey)); + })); } } diff --git a/test/parallel/test-crypto-keygen-rfc8017-9-1.js b/test/parallel/test-crypto-keygen-rfc8017-9-1.js index fbefb1b4f642..3234084afe10 100644 --- a/test/parallel/test-crypto-keygen-rfc8017-9-1.js +++ b/test/parallel/test-crypto-keygen-rfc8017-9-1.js @@ -11,19 +11,20 @@ const assert = require('assert'); const { generateKeyPair, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); // RFC 8017, 9.1.: "Assuming that the mask generation function is based on a // hash function, it is RECOMMENDED that the hash function be the same as the // one that is applied to the message." { - + const modulusLength = hasFIPS(3) ? 2048 : 512; generateKeyPair('rsa-pss', { - modulusLength: 512, + modulusLength, hashAlgorithm: 'sha256', saltLength: 16 }, common.mustSucceed((publicKey, privateKey) => { const expectedKeyDetails = { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', diff --git a/test/parallel/test-crypto-keygen-rfc8017-a-2-3.js b/test/parallel/test-crypto-keygen-rfc8017-a-2-3.js index bc96d57ed0cd..f7cb70560e7e 100644 --- a/test/parallel/test-crypto-keygen-rfc8017-a-2-3.js +++ b/test/parallel/test-crypto-keygen-rfc8017-a-2-3.js @@ -11,16 +11,18 @@ const assert = require('assert'); const { generateKeyPair, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); // RFC 8017, A.2.3.: "For a given hashAlgorithm, the default value of // saltLength is the octet length of the hash value." { + const modulusLength = hasFIPS(3) ? 2048 : 512; generateKeyPair('rsa-pss', { - modulusLength: 512, + modulusLength, hashAlgorithm: 'sha512' }, common.mustSucceed((publicKey, privateKey) => { const expectedKeyDetails = { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha512', mgf1HashAlgorithm: 'sha512', @@ -32,12 +34,12 @@ const { // It is still possible to explicitly set saltLength to 0. generateKeyPair('rsa-pss', { - modulusLength: 512, + modulusLength, hashAlgorithm: 'sha512', saltLength: 0 }, common.mustSucceed((publicKey, privateKey) => { const expectedKeyDetails = { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha512', mgf1HashAlgorithm: 'sha512', diff --git a/test/parallel/test-crypto-keygen-rsa-pss.js b/test/parallel/test-crypto-keygen-rsa-pss.js index 3ce0d40e8d1b..88d522a223b9 100644 --- a/test/parallel/test-crypto-keygen-rsa-pss.js +++ b/test/parallel/test-crypto-keygen-rsa-pss.js @@ -13,14 +13,16 @@ const { generateKeyPair, } = require('crypto'); const { + hasFIPS, testEncryptDecrypt, testSignVerify, } = require('../common/crypto'); // Test RSA-PSS. { + const modulusLength = hasFIPS(3) ? 2048 : 512; generateKeyPair('rsa-pss', { - modulusLength: 512, + modulusLength, saltLength: 16, hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256' @@ -28,7 +30,7 @@ const { assert.strictEqual(publicKey.type, 'public'); assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss'); assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', @@ -38,7 +40,7 @@ const { assert.strictEqual(privateKey.type, 'private'); assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss'); assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { - modulusLength: 512, + modulusLength, publicExponent: 65537n, hashAlgorithm: 'sha256', mgf1HashAlgorithm: 'sha256', diff --git a/test/parallel/test-crypto-keygen-sync.js b/test/parallel/test-crypto-keygen-sync.js index a100379e21f1..54a91fcfbcae 100644 --- a/test/parallel/test-crypto-keygen-sync.js +++ b/test/parallel/test-crypto-keygen-sync.js @@ -10,6 +10,7 @@ const { } = require('crypto'); const { assertApproximateSize, + hasFIPS, testEncryptDecrypt, testSignVerify, pkcs1PubExp, @@ -19,9 +20,10 @@ const { // To make the test faster, we will only test sync key generation once and // with a relatively small key. { + const isFips = hasFIPS(3); const ret = generateKeyPairSync('rsa', { - publicExponent: 3, - modulusLength: 512, + publicExponent: isFips ? 0x10001 : 3, + modulusLength: isFips ? 2048 : 512, publicKeyEncoding: { type: 'pkcs1', format: 'pem' @@ -37,10 +39,10 @@ const { assert.strictEqual(typeof publicKey, 'string'); assert.match(publicKey, pkcs1PubExp); - assertApproximateSize(publicKey, 162); + assertApproximateSize(publicKey, isFips ? 426 : 162); assert.strictEqual(typeof privateKey, 'string'); assert.match(privateKey, pkcs8Exp); - assertApproximateSize(privateKey, 512); + assertApproximateSize(privateKey, isFips ? 1704 : 512); testEncryptDecrypt(publicKey, privateKey); testSignVerify(publicKey, privateKey); diff --git a/test/parallel/test-crypto-keyobject-brand-check.js b/test/parallel/test-crypto-keyobject-brand-check.js index ac0cf1b65f70..ed8e5bdae76e 100644 --- a/test/parallel/test-crypto-keyobject-brand-check.js +++ b/test/parallel/test-crypto-keyobject-brand-check.js @@ -15,6 +15,7 @@ const { generateKeyPairSync, KeyObject, } = require('node:crypto'); +const { hasFIPS } = require('../common/crypto'); const { types: { isKeyObject } } = require('node:util'); const invalidThis = { code: 'ERR_INVALID_THIS', name: 'TypeError' }; @@ -25,7 +26,9 @@ function getter(proto, name) { { const secret = createSecretKey(Buffer.alloc(16)); - const { publicKey } = generateKeyPairSync('rsa', { modulusLength: 1024 }); + const { publicKey } = generateKeyPairSync('rsa', { + modulusLength: hasFIPS(3) ? 2048 : 1024, + }); const type = getter(KeyObject.prototype, 'type'); const symmetricKeySize = diff --git a/test/parallel/test-crypto-keyobject-clone-transfer.js b/test/parallel/test-crypto-keyobject-clone-transfer.js index 61cfcdc497f8..2ac37ca201e7 100644 --- a/test/parallel/test-crypto-keyobject-clone-transfer.js +++ b/test/parallel/test-crypto-keyobject-clone-transfer.js @@ -16,6 +16,7 @@ const { sign, verify, } = require('node:crypto'); +const { hasFIPS } = require('../common/crypto'); const { MessageChannel, Worker } = require('node:worker_threads'); const { types: { isKeyObject } } = require('node:util'); @@ -87,7 +88,7 @@ function hmacDigest(key) { (async () => { const secret = createSecretKey(Buffer.alloc(16)); const { publicKey, privateKey } = generateKeyPairSync('rsa', { - modulusLength: 1024, + modulusLength: hasFIPS(3) ? 2048 : 1024, }); for (const key of [secret, publicKey, privateKey]) { diff --git a/test/parallel/test-crypto-keyobject-hidden-slots.js b/test/parallel/test-crypto-keyobject-hidden-slots.js index 1ea243ba0ab8..b3db2068541f 100644 --- a/test/parallel/test-crypto-keyobject-hidden-slots.js +++ b/test/parallel/test-crypto-keyobject-hidden-slots.js @@ -29,6 +29,7 @@ const { X509Certificate, } = require('node:crypto'); const { readFileSync } = require('node:fs'); +const { hasFIPS } = require('../common/crypto'); const fixtures = require('../common/fixtures'); function updateFinal(cipher, data = Buffer.alloc(16)) { @@ -110,12 +111,13 @@ function updateFinal(cipher, data = Buffer.alloc(16)) { } { + const modulusLength = hasFIPS(3) ? 2048 : 1024; const { publicKey } = generateKeyPairSync('rsa', { - modulusLength: 1024, + modulusLength, }); const details = publicKey.asymmetricKeyDetails; - assert.strictEqual(details.modulusLength, 1024); + assert.strictEqual(details.modulusLength, modulusLength); assert.strictEqual(details.publicExponent, 65537n); details.modulusLength = 1; @@ -124,7 +126,7 @@ function updateFinal(cipher, data = Buffer.alloc(16)) { const freshDetails = publicKey.asymmetricKeyDetails; assert.notStrictEqual(freshDetails, details); - assert.strictEqual(freshDetails.modulusLength, 1024); + assert.strictEqual(freshDetails.modulusLength, modulusLength); assert.strictEqual(freshDetails.publicExponent, 65537n); assert.strictEqual(freshDetails.extra, undefined); } @@ -150,7 +152,7 @@ function updateFinal(cipher, data = Buffer.alloc(16)) { { const { privateKey, publicKey } = generateKeyPairSync('rsa', { - modulusLength: 1024, + modulusLength: hasFIPS(3) ? 2048 : 1024, }); const originalType = Object.getOwnPropertyDescriptor(KeyObject.prototype, 'type'); @@ -175,8 +177,10 @@ function updateFinal(cipher, data = Buffer.alloc(16)) { verifier.update(data); assert.strictEqual(verifier.verify(publicKey, streamSignature), true); - const ciphertext = publicEncrypt(publicKey, data); - assert.deepStrictEqual(privateDecrypt(privateKey, ciphertext), data); + const options = hasFIPS(3) ? { oaepHash: 'sha256' } : {}; + const ciphertext = publicEncrypt({ key: publicKey, ...options }, data); + assert.deepStrictEqual( + privateDecrypt({ key: privateKey, ...options }, ciphertext), data); assert.strictEqual(publicKey.equals(createPublicKey(privateKey)), true); diff --git a/test/parallel/test-crypto-keyobject-no-own-symbols.js b/test/parallel/test-crypto-keyobject-no-own-symbols.js index f1539c6a0f7a..945032446784 100644 --- a/test/parallel/test-crypto-keyobject-no-own-symbols.js +++ b/test/parallel/test-crypto-keyobject-no-own-symbols.js @@ -12,6 +12,7 @@ const { createSecretKey, generateKeyPairSync, } = require('node:crypto'); +const { hasFIPS } = require('../common/crypto'); function assertNoOwnKeys(key) { assert.deepStrictEqual(Object.getOwnPropertySymbols(key), []); @@ -22,7 +23,7 @@ function assertNoOwnKeys(key) { { const secret = createSecretKey(Buffer.alloc(16)); const { publicKey, privateKey } = generateKeyPairSync('rsa', { - modulusLength: 1024, + modulusLength: hasFIPS(3) ? 2048 : 1024, }); for (const key of [secret, publicKey, privateKey]) { diff --git a/test/parallel/test-crypto-pbkdf2.js b/test/parallel/test-crypto-pbkdf2.js index 78b73ed6c4e0..7cd1206f4f02 100644 --- a/test/parallel/test-crypto-pbkdf2.js +++ b/test/parallel/test-crypto-pbkdf2.js @@ -5,7 +5,16 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { + hasOpenSSL, + hasFIPS, +} = require('../common/crypto'); + +const fips4 = hasFIPS(4); +const validPassword = fips4 ? 'password' : 'pass'; +const validSalt = fips4 ? '0123456789abcdef' : 'salt'; +const validIterations = fips4 ? 1000 : 8; +const validKeyLength = fips4 ? 16 : 8; function runPBKDF2(password, salt, iterations, keylen, hash) { const syncResult = @@ -19,6 +28,25 @@ function runPBKDF2(password, salt, iterations, keylen, hash) { return syncResult; } +function assertPBKDF2Fails(password, salt, iterations, keylen, hash) { + const expected = { message: 'PBKDF2 derivation failed' }; + assert.throws( + () => crypto.pbkdf2Sync(password, salt, iterations, keylen, hash), + expected, + ); + crypto.pbkdf2( + password, + salt, + iterations, + keylen, + hash, + common.mustCall((err, result) => { + assert.strictEqual(err?.message, expected.message); + assert.strictEqual(result, undefined); + }), + ); +} + function testPBKDF2(password, salt, iterations, keylen, expected, encoding) { const actual = runPBKDF2(password, salt, iterations, keylen, 'sha256'); assert.strictEqual(actual.toString(encoding || 'latin1'), expected); @@ -28,36 +56,55 @@ function testPBKDF2(password, salt, iterations, keylen, expected, encoding) { // Test PBKDF2 with RFC 6070 test vectors (except #4) // -testPBKDF2('password', 'salt', 1, 20, - '\x12\x0f\xb6\xcf\xfc\xf8\xb3\x2c\x43\xe7\x22\x52' + - '\x56\xc4\xf8\x37\xa8\x65\x48\xc9'); - -testPBKDF2('password', 'salt', 2, 20, - '\xae\x4d\x0c\x95\xaf\x6b\x46\xd3\x2d\x0a\xdf\xf9' + - '\x28\xf0\x6d\xd0\x2a\x30\x3f\x8e'); - -testPBKDF2('password', 'salt', 4096, 20, - '\xc5\xe4\x78\xd5\x92\x88\xc8\x41\xaa\x53\x0d\xb6' + - '\x84\x5c\x4c\x8d\x96\x28\x93\xa0'); - -testPBKDF2('passwordPASSWORDpassword', - 'saltSALTsaltSALTsaltSALTsaltSALTsalt', - 4096, - 25, - '\x34\x8c\x89\xdb\xcb\xd3\x2b\x2f\x32\xd8\x14\xb8\x11' + - '\x6e\x84\xcf\x2b\x17\x34\x7e\xbc\x18\x00\x18\x1c'); - -testPBKDF2('pass\0word', 'sa\0lt', 4096, 16, - '\x89\xb6\x9d\x05\x16\xf8\x29\x89\x3c\x69\x62\x26\x65' + - '\x0a\x86\x87'); - -testPBKDF2('password', 'salt', 32, 32, - '64c486c55d30d4c5a079b8823b7d7cb37ff0556f537da8410233bcec330ed956', - 'hex'); +if (fips4) { + testPBKDF2(validPassword, validSalt, validIterations, 32, + '8514638175a45bc45eb1f22f04ff7d27' + + 'f4f8be480498c455ff4b494ce8d1e7d2', + 'hex'); + + for (const args of [ + ['short', validSalt, validIterations], + [validPassword, 'short', validIterations], + [validPassword, validSalt, 999], + ]) { + assertPBKDF2Fails(...args, 32, 'sha256'); + } + assertPBKDF2Fails( + validPassword, validSalt, validIterations, 8, 'sha256'); +} else { + testPBKDF2('password', 'salt', 1, 20, + '\x12\x0f\xb6\xcf\xfc\xf8\xb3\x2c\x43\xe7\x22\x52' + + '\x56\xc4\xf8\x37\xa8\x65\x48\xc9'); + + testPBKDF2('password', 'salt', 2, 20, + '\xae\x4d\x0c\x95\xaf\x6b\x46\xd3\x2d\x0a\xdf\xf9' + + '\x28\xf0\x6d\xd0\x2a\x30\x3f\x8e'); + + testPBKDF2('password', 'salt', 4096, 20, + '\xc5\xe4\x78\xd5\x92\x88\xc8\x41\xaa\x53\x0d\xb6' + + '\x84\x5c\x4c\x8d\x96\x28\x93\xa0'); + + testPBKDF2('passwordPASSWORDpassword', + 'saltSALTsaltSALTsaltSALTsaltSALTsalt', + 4096, + 25, + '\x34\x8c\x89\xdb\xcb\xd3\x2b\x2f\x32\xd8\x14\xb8\x11' + + '\x6e\x84\xcf\x2b\x17\x34\x7e\xbc\x18\x00\x18\x1c'); + + testPBKDF2('pass\0word', 'sa\0lt', 4096, 16, + '\x89\xb6\x9d\x05\x16\xf8\x29\x89\x3c\x69\x62\x26\x65' + + '\x0a\x86\x87'); + + testPBKDF2('password', 'salt', 32, 32, + '64c486c55d30d4c5a079b8823b7d7cb3' + + '7ff0556f537da8410233bcec330ed956', + 'hex'); +} // Error path should not leak memory (check with valgrind). assert.throws( - () => crypto.pbkdf2('password', 'salt', 1, 20, 'sha1'), + () => crypto.pbkdf2( + validPassword, validSalt, validIterations, 20, 'sha1'), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' @@ -66,7 +113,8 @@ assert.throws( for (const iterations of [-1, 0, 2147483648]) { assert.throws( - () => crypto.pbkdf2Sync('password', 'salt', iterations, 20, 'sha1'), + () => crypto.pbkdf2Sync( + validPassword, validSalt, iterations, 20, 'sha1'), { code: 'ERR_OUT_OF_RANGE', name: 'RangeError', @@ -77,7 +125,8 @@ for (const iterations of [-1, 0, 2147483648]) { ['str', null, undefined, [], {}].forEach((notNumber) => { assert.throws( () => { - crypto.pbkdf2Sync('password', 'salt', 1, notNumber, 'sha256'); + crypto.pbkdf2Sync( + validPassword, validSalt, validIterations, notNumber, 'sha256'); }, { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -89,7 +138,7 @@ for (const iterations of [-1, 0, 2147483648]) { [Infinity, -Infinity, NaN].forEach((input) => { assert.throws( () => { - crypto.pbkdf2('password', 'salt', 1, input, 'sha256', + crypto.pbkdf2(validPassword, validSalt, validIterations, input, 'sha256', common.mustNotCall()); }, { code: 'ERR_OUT_OF_RANGE', @@ -102,7 +151,7 @@ for (const iterations of [-1, 0, 2147483648]) { [-1, 2147483648, 4294967296].forEach((input) => { assert.throws( () => { - crypto.pbkdf2('password', 'salt', 1, input, 'sha256', + crypto.pbkdf2(validPassword, validSalt, validIterations, input, 'sha256', common.mustNotCall()); }, { code: 'ERR_OUT_OF_RANGE', @@ -119,14 +168,16 @@ for (const iterations of [-1, 0, 2147483648]) { let posError; let posResult; try { - posResult = crypto.pbkdf2Sync('password', 'salt', 1, 0, 'sha256'); + posResult = crypto.pbkdf2Sync( + validPassword, validSalt, validIterations, 0, 'sha256'); } catch (err) { posError = err; } let negError; let negResult; try { - negResult = crypto.pbkdf2Sync('password', 'salt', 1, -0, 'sha256'); + negResult = crypto.pbkdf2Sync( + validPassword, validSalt, validIterations, -0, 'sha256'); } catch (err) { negError = err; } @@ -136,15 +187,22 @@ for (const iterations of [-1, 0, 2147483648]) { assert.deepStrictEqual(negResult, posResult); } - crypto.pbkdf2('password', 'salt', 1, -0, 'sha256', common.mustCall()); + crypto.pbkdf2( + validPassword, validSalt, validIterations, -0, 'sha256', + common.mustCall()); } // Should not get FATAL ERROR with empty password and salt // https://github.com/nodejs/node/issues/8571 -crypto.pbkdf2('', '', 1, 32, 'sha256', common.mustSucceed()); +if (fips4) { + assertPBKDF2Fails('', '', 1, 32, 'sha256'); +} else { + crypto.pbkdf2('', '', 1, 32, 'sha256', common.mustSucceed()); +} assert.throws( - () => crypto.pbkdf2('password', 'salt', 8, 8, common.mustNotCall()), + () => crypto.pbkdf2( + validPassword, validSalt, validIterations, 8, common.mustNotCall()), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -153,7 +211,7 @@ assert.throws( }); assert.throws( - () => crypto.pbkdf2Sync('password', 'salt', 8, 8), + () => crypto.pbkdf2Sync(validPassword, validSalt, validIterations, 8), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -162,7 +220,7 @@ assert.throws( }); assert.throws( - () => crypto.pbkdf2Sync('password', 'salt', 8, 8, null), + () => crypto.pbkdf2Sync(validPassword, validSalt, validIterations, 8, null), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -171,7 +229,8 @@ assert.throws( }); [1, {}, [], true, undefined, null].forEach((input) => { assert.throws( - () => crypto.pbkdf2(input, 'salt', 8, 8, 'sha256', common.mustNotCall()), + () => crypto.pbkdf2( + input, validSalt, validIterations, 8, 'sha256', common.mustNotCall()), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -179,7 +238,9 @@ assert.throws( ); assert.throws( - () => crypto.pbkdf2('pass', input, 8, 8, 'sha256', common.mustNotCall()), + () => crypto.pbkdf2( + validPassword, input, validIterations, 8, 'sha256', + common.mustNotCall()), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -187,7 +248,8 @@ assert.throws( ); assert.throws( - () => crypto.pbkdf2Sync(input, 'salt', 8, 8, 'sha256'), + () => crypto.pbkdf2Sync( + input, validSalt, validIterations, 8, 'sha256'), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -195,7 +257,8 @@ assert.throws( ); assert.throws( - () => crypto.pbkdf2Sync('pass', input, 8, 8, 'sha256'), + () => crypto.pbkdf2Sync( + validPassword, input, validIterations, 8, 'sha256'), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -206,7 +269,8 @@ assert.throws( ['test', {}, [], true, undefined, null].forEach((i) => { const received = common.invalidArgTypeHelper(i); assert.throws( - () => crypto.pbkdf2('pass', 'salt', i, 8, 'sha256', common.mustNotCall()), + () => crypto.pbkdf2( + validPassword, validSalt, i, 8, 'sha256', common.mustNotCall()), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -215,7 +279,7 @@ assert.throws( ); assert.throws( - () => crypto.pbkdf2Sync('pass', 'salt', i, 8, 'sha256'), + () => crypto.pbkdf2Sync(validPassword, validSalt, i, 8, 'sha256'), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -224,15 +288,32 @@ assert.throws( ); }); +if (fips4) { + assertPBKDF2Fails( + new Uint8Array(1), validSalt, validIterations, validKeyLength, 'sha256'); + assertPBKDF2Fails( + validPassword, new Uint8Array(1), validIterations, validKeyLength, + 'sha256'); +} + // Any TypedArray should work for password and salt. for (const SomeArray of [Uint8Array, Uint16Array, Uint32Array, Float32Array, Float64Array, ArrayBuffer, SharedArrayBuffer]) { - runPBKDF2(new SomeArray(10), 'salt', 8, 8, 'sha256'); - runPBKDF2('pass', new SomeArray(10), 8, 8, 'sha256'); + const length = fips4 ? 16 : 10; + const input = new SomeArray(length); + const bytes = ArrayBuffer.isView(input) ? + new Uint8Array(input.buffer, input.byteOffset, input.byteLength) : + new Uint8Array(input); + for (let index = 0; index < bytes.length; index++) + bytes[index] = index + 1; + runPBKDF2(input, validSalt, validIterations, validKeyLength, 'sha256'); + runPBKDF2(validPassword, input, validIterations, validKeyLength, 'sha256'); } assert.throws( - () => crypto.pbkdf2('pass', 'salt', 8, 8, 'md55', common.mustNotCall()), + () => crypto.pbkdf2( + validPassword, validSalt, validIterations, 8, 'md55', + common.mustNotCall()), { code: 'ERR_CRYPTO_INVALID_DIGEST', name: 'TypeError', @@ -241,7 +322,8 @@ assert.throws( ); assert.throws( - () => crypto.pbkdf2Sync('pass', 'salt', 8, 8, 'md55'), + () => crypto.pbkdf2Sync( + validPassword, validSalt, validIterations, 8, 'md55'), { code: 'ERR_CRYPTO_INVALID_DIGEST', name: 'TypeError', @@ -249,7 +331,7 @@ assert.throws( } ); -if (!hasOpenSSL3) { +if (!hasOpenSSL(3)) { const kNotPBKDF2Supported = ['shake128', 'shake256']; crypto.getHashes() .filter((hash) => !kNotPBKDF2Supported.includes(hash)) @@ -261,7 +343,8 @@ if (!hasOpenSSL3) { { // This should not crash. assert.throws( - () => crypto.pbkdf2Sync('1', '2', 1, 1, '%'), + () => crypto.pbkdf2Sync( + validPassword, validSalt, validIterations, 1, '%'), { code: 'ERR_CRYPTO_INVALID_DIGEST', name: 'TypeError', diff --git a/test/parallel/test-crypto-pqc-encrypted-pkcs8.js b/test/parallel/test-crypto-pqc-encrypted-pkcs8.js index b4a1b586d21d..8f2a10725c94 100644 --- a/test/parallel/test-crypto-pqc-encrypted-pkcs8.js +++ b/test/parallel/test-crypto-pqc-encrypted-pkcs8.js @@ -4,7 +4,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) common.skip('requires OpenSSL >= 3.5 or BoringSSL'); @@ -30,19 +30,32 @@ if (process.features.openssl_is_boringssl) { // ciphers like RC2, the optional PBKDF2 keyLength INTEGER branch in // the EncryptedPrivateKeyInfo parser. const availableCiphers = new Set(getCiphers()); +const passphrase = 'top secret'; const ciphers = [ 'aes-128-cbc', 'aes-192-cbc', 'aes-256-cbc', 'des-ede3-cbc', 'rc2-cbc', -].filter((c) => availableCiphers.has(c)); +].filter((cipher) => availableCiphers.has(cipher) && + (!hasFIPS(3) || cipher !== 'rc2-cbc')); + +if (hasFIPS(3)) { + const { privateKey } = generateKeyPairSync('ml-dsa-44'); + assert.throws(() => privateKey.export({ + type: 'pkcs8', + format: 'der', + cipher: 'rc2-cbc', + passphrase, + }), { code: 'ERR_OSSL_EVP_UNSUPPORTED' }); +} -const passphrase = 'top secret'; +const wrongPassphrase = 'wrong password'; const wrongPassphraseError = /bad decrypt|DECRYPTION_FAILED|BAD_DECRYPT|bad password|DECODE[ _]ERROR/i; // A wrong passphrase usually fails during cipher finalization, but CBC output // can have valid padding by chance. OpenSSL then parses the bad plaintext as // PKCS#8 and may report ASN.1 or decoder errors from the same failed import. function assertWrongPassphrase(fn) { - assert.throws(fn, (err) => wrongPassphraseError.test(err.message) || + assert.throws(fn, (err) => err.code === 'ERR_OSSL_BAD_DECRYPT' || + wrongPassphraseError.test(err.message) || err.code?.startsWith('ERR_OSSL_ASN1_') || err.code === 'ERR_OSSL_UNSUPPORTED'); } @@ -79,7 +92,7 @@ for (const asymmetricKeyType of algorithms) { key: encrypted, format, type: 'pkcs8', - passphrase: 'wrong', + passphrase: wrongPassphrase, })); } } @@ -128,7 +141,7 @@ for (const { alg, jwkFile, encBase } of fixtureCases) { key: encryptedFixture, format, type: 'pkcs8', - passphrase: 'wrong', + passphrase: wrongPassphrase, })); } } diff --git a/test/parallel/test-crypto-private-decrypt-gh32240.js b/test/parallel/test-crypto-private-decrypt-gh32240.js index 1ff5b565d6d5..a38fcba6775e 100644 --- a/test/parallel/test-crypto-private-decrypt-gh32240.js +++ b/test/parallel/test-crypto-private-decrypt-gh32240.js @@ -14,29 +14,57 @@ const { privateDecrypt, } = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { + hasOpenSSL, + hasFIPS, +} = require('../common/crypto'); -const pair = generateKeyPairSync('rsa', { modulusLength: 512 }); +const fips3 = hasFIPS(3); +const fips4 = hasFIPS(4); +const pair = generateKeyPairSync('rsa', { + modulusLength: fips3 ? 2048 : 512, +}); const expected = Buffer.from('shibboleth'); -const encrypted = publicEncrypt(pair.publicKey, expected); +const options = fips3 ? { oaepHash: 'sha256' } : {}; +const encrypted = publicEncrypt({ key: pair.publicKey, ...options }, expected); const pkey = pair.privateKey.export({ type: 'pkcs1', format: 'pem' }); -const pkeyEncrypted = - pair.privateKey.export({ +if (fips3) { + assert.throws(() => pair.privateKey.export({ type: 'pkcs1', format: 'pem', cipher: 'aes-128-cbc', passphrase: 'secret', + }), { + code: 'ERR_OSSL_EVP_UNSUPPORTED', + }); +} +if (fips4) { + assert.throws(() => pair.privateKey.export({ + type: 'pkcs8', + format: 'pem', + cipher: 'aes-256-cbc', + passphrase: 'secret', + }), { + code: 'ERR_OSSL_PASSWORD_STRENGTH_TOO_WEAK', + }); +} +const pkeyEncrypted = + pair.privateKey.export({ + type: fips3 ? 'pkcs8' : 'pkcs1', + format: 'pem', + cipher: fips3 ? 'aes-256-cbc' : 'aes-128-cbc', + passphrase: 'password', }); function decrypt(key) { - const decrypted = privateDecrypt(key, encrypted); + const decrypted = privateDecrypt({ key, ...options }, encrypted); assert.deepStrictEqual(decrypted, expected); } decrypt(pkey); -assert.throws(() => decrypt(pkeyEncrypted), hasOpenSSL3 ? +assert.throws(() => decrypt(pkeyEncrypted), hasOpenSSL(3) ? { message: 'error:07880109:common libcrypto routines::interrupted or ' + 'cancelled' } : { code: 'ERR_MISSING_PASSPHRASE' }); diff --git a/test/parallel/test-crypto-publicDecrypt-fails-first-time.js b/test/parallel/test-crypto-publicDecrypt-fails-first-time.js index 1d64e08920c6..21cc5f3ebce2 100644 --- a/test/parallel/test-crypto-publicDecrypt-fails-first-time.js +++ b/test/parallel/test-crypto-publicDecrypt-fails-first-time.js @@ -7,15 +7,17 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL3) { +if (!hasOpenSSL(3)) { common.skip('only openssl3'); // https://github.com/nodejs/node/pull/42793#issuecomment-1107491901 } const assert = require('assert'); const crypto = require('crypto'); +const passphrase = 'password'; + const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { @@ -26,7 +28,7 @@ const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { type: 'pkcs8', format: 'pem', cipher: 'aes-128-ecb', - passphrase: 'abcdef' + passphrase } }); assert.notStrictEqual(privateKey.toString(), ''); @@ -35,7 +37,7 @@ const msg = 'The quick brown fox jumps over the lazy dog'; const encryptedString = crypto.privateEncrypt({ key: privateKey, - passphrase: 'abcdef' + passphrase }, Buffer.from(msg)).toString('base64'); const decryptedString = crypto.publicDecrypt(publicKey, Buffer.from(encryptedString, 'base64')).toString(); console.log(`Encrypted: ${encryptedString}`); diff --git a/test/parallel/test-crypto-rsa-dsa.js b/test/parallel/test-crypto-rsa-dsa.js index 35ad67302077..bda98652cf59 100644 --- a/test/parallel/test-crypto-rsa-dsa.js +++ b/test/parallel/test-crypto-rsa-dsa.js @@ -9,7 +9,16 @@ const crypto = require('crypto'); const constants = crypto.constants; const fixtures = require('../common/fixtures'); -const { hasOpenSSL, hasOpenSSL3 } = require('../common/crypto'); +const { + hasOpenSSL, + hasFIPS, +} = require('../common/crypto'); +const fips3 = hasFIPS(3); +const fips35 = hasFIPS(3, 5); +const fips30 = fips3 && !fips35; +const fips4 = hasFIPS(4); +const fipsDigestErrorCode = 'ERR_OSSL_DIGEST_NOT_ALLOWED'; +const wrongPassphrase = 'wrong-password'; // Test certificates const certPem = fixtures.readKey('rsa_cert.crt'); @@ -17,17 +26,42 @@ const keyPem = fixtures.readKey('rsa_private.pem'); const rsaKeySize = 2048; const rsaPubPem = fixtures.readKey('rsa_public.pem', 'ascii'); const rsaKeyPem = fixtures.readKey('rsa_private.pem', 'ascii'); -const rsaKeyPemEncrypted = fixtures.readKey('rsa_private_encrypted.pem', - 'ascii'); +const rsaKeyPemEncryptedLegacy = fixtures.readKey( + 'rsa_private_encrypted.pem', 'ascii'); +const rsaKeyPemEncrypted = fips3 ? + crypto.createPrivateKey(rsaKeyPem).export({ + type: 'pkcs8', + format: 'pem', + cipher: 'aes-256-cbc', + passphrase: 'password', + }) : rsaKeyPemEncryptedLegacy; const dsaPubPem = fixtures.readKey('dsa_public.pem', 'ascii'); const dsaKeyPem = fixtures.readKey('dsa_private.pem', 'ascii'); -const dsaKeyPemEncrypted = fixtures.readKey('dsa_private_encrypted.pem', - 'ascii'); +const dsaKeyPemEncryptedLegacy = fixtures.readKey( + 'dsa_private_encrypted.pem', 'ascii'); +const dsaKeyPemEncrypted = fips3 ? + crypto.createPrivateKey(dsaKeyPem).export({ + type: 'pkcs8', + format: 'pem', + cipher: 'aes-256-cbc', + passphrase: 'password', + }) : dsaKeyPemEncryptedLegacy; const rsaPkcs8KeyPem = fixtures.readKey('rsa_private_pkcs8.pem'); const dsaPkcs8KeyPem = fixtures.readKey('dsa_private_pkcs8.pem'); const ec = new TextEncoder(); +if (fips3) { + for (const key of [rsaKeyPemEncryptedLegacy, dsaKeyPemEncryptedLegacy]) { + assert.throws(() => crypto.createPrivateKey({ + key, + passphrase: 'password', + }), { + code: 'ERR_OSSL_EVP_UNSUPPORTED', + }); + } +} + const openssl1DecryptError = { message: 'error:06065064:digital envelope routines:EVP_DecryptFinal_ex:' + 'bad decrypt', @@ -37,18 +71,21 @@ const openssl1DecryptError = { library: 'digital envelope routines', }; -const decryptError = hasOpenSSL3 ? - { message: 'error:1C800064:Provider routines::bad decrypt' } : - process.features.openssl_is_boringssl ? { - message: 'error:1e000065:Cipher functions:OPENSSL_internal:BAD_DECRYPT', - code: 'ERR_OSSL_BAD_DECRYPT', - reason: 'BAD_DECRYPT', - function: 'OPENSSL_internal', - library: 'Cipher functions', - } : - openssl1DecryptError; - -const decryptPrivateKeyError = hasOpenSSL3 ? { +const decryptError = fips4 ? + { code: 'ERR_OSSL_BAD_DECRYPT' } : hasOpenSSL(3) ? + { message: 'error:1C800064:Provider routines::bad decrypt' } : + process.features.openssl_is_boringssl ? { + message: 'error:1e000065:Cipher functions:OPENSSL_internal:BAD_DECRYPT', + code: 'ERR_OSSL_BAD_DECRYPT', + reason: 'BAD_DECRYPT', + function: 'OPENSSL_internal', + library: 'Cipher functions', + } : + openssl1DecryptError; + +const decryptPrivateKeyError = fips4 ? { + code: 'ERR_OSSL_BAD_DECRYPT', +} : hasOpenSSL(3) ? { message: 'error:1C800064:Provider routines::bad decrypt', } : process.features.openssl_is_boringssl ? { message: 'error:1e000065:Cipher functions:OPENSSL_internal:BAD_DECRYPT', @@ -156,7 +193,7 @@ function getBufferCopy(buf) { // Now with RSA_NO_PADDING. Plaintext needs to match key size. // OpenSSL 3.x has a rsa_check_padding that will cause an error if // RSA_NO_PADDING is used. - if (!hasOpenSSL3) { + if (!hasOpenSSL(3)) { { const plaintext = 'x'.repeat(rsaKeySize / 8); encryptedBuffer = crypto.privateEncrypt({ @@ -192,14 +229,14 @@ function getBufferCopy(buf) { assert.throws(() => { crypto.privateDecrypt({ key: rsaKeyPemEncrypted, - passphrase: 'wrong' + passphrase: wrongPassphrase }, bufferToEncrypt); }, decryptError); assert.throws(() => { crypto.publicEncrypt({ key: rsaKeyPemEncrypted, - passphrase: 'wrong' + passphrase: wrongPassphrase }, encryptedBuffer); }, decryptError); @@ -211,7 +248,7 @@ function getBufferCopy(buf) { assert.throws(() => { crypto.publicDecrypt({ key: rsaKeyPemEncrypted, - passphrase: Buffer.from('wrong') + passphrase: Buffer.from(wrongPassphrase) }, encryptedBuffer); }, decryptError); } @@ -349,8 +386,12 @@ test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha256', 'sha256'); test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha512', 'sha512'); assert.throws(() => { test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha256', 'sha512'); -}, { - code: 'ERR_OSSL_RSA_OAEP_DECODING_ERROR' +}, fips35 ? { + code: 'ERR_OSSL_EVP_PROVIDER_ASYM_CIPHER_FAILURE', +} : fips3 ? { + message: 'error:00000000:lib(0)::reason(0)', +} : { + code: 'ERR_OSSL_RSA_OAEP_DECODING_ERROR', }); // The following RSA-OAEP test cases were created using the WebCrypto API to @@ -416,8 +457,9 @@ for (const fn of [crypto.publicEncrypt, crypto.privateDecrypt]) { } // Test RSA key signing/verification -let rsaSign = crypto.createSign('SHA1'); -let rsaVerify = crypto.createVerify('SHA1'); +const rsaDigest = fips3 ? 'SHA256' : 'SHA1'; +let rsaSign = crypto.createSign(rsaDigest); +let rsaVerify = crypto.createVerify(rsaDigest); assert.ok(rsaSign); assert.ok(rsaVerify); @@ -428,36 +470,39 @@ const expectedSignature = fixtures.readKey( rsaSign.update(rsaPubPem); let rsaSignature = rsaSign.sign(rsaKeyPem, 'hex'); -assert.strictEqual(rsaSignature, expectedSignature); +if (!fips3) + assert.strictEqual(rsaSignature, expectedSignature); rsaVerify.update(rsaPubPem); assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true); // Test RSA PKCS#8 key signing/verification -rsaSign = crypto.createSign('SHA1'); +rsaSign = crypto.createSign(rsaDigest); rsaSign.update(rsaPubPem); rsaSignature = rsaSign.sign(rsaPkcs8KeyPem, 'hex'); -assert.strictEqual(rsaSignature, expectedSignature); +if (!fips3) + assert.strictEqual(rsaSignature, expectedSignature); -rsaVerify = crypto.createVerify('SHA1'); +rsaVerify = crypto.createVerify(rsaDigest); rsaVerify.update(rsaPubPem); assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true); // Test RSA key signing/verification with encrypted key -rsaSign = crypto.createSign('SHA1'); +rsaSign = crypto.createSign(rsaDigest); rsaSign.update(rsaPubPem); const signOptions = { key: rsaKeyPemEncrypted, passphrase: 'password' }; rsaSignature = rsaSign.sign(signOptions, 'hex'); -assert.strictEqual(rsaSignature, expectedSignature); +if (!fips3) + assert.strictEqual(rsaSignature, expectedSignature); -rsaVerify = crypto.createVerify('SHA1'); +rsaVerify = crypto.createVerify(rsaDigest); rsaVerify.update(rsaPubPem); assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true); -rsaSign = crypto.createSign('SHA1'); +rsaSign = crypto.createSign(rsaDigest); rsaSign.update(rsaPubPem); assert.throws(() => { - const signOptions = { key: rsaKeyPemEncrypted, passphrase: 'wrong' }; + const signOptions = { key: rsaKeyPemEncrypted, passphrase: wrongPassphrase }; rsaSign.sign(signOptions, 'hex'); }, decryptPrivateKeyError); @@ -508,11 +553,12 @@ if (!process.features.openssl_is_boringssl) { // DSA signatures vary across runs so there is no static string to verify // against. - const sign = crypto.createSign('SHA1'); + const dsaDigest = fips3 ? 'SHA256' : 'SHA1'; + const sign = crypto.createSign(dsaDigest); sign.update(input); const signature = sign.sign(dsaKeyPem, 'hex'); - const verify = crypto.createVerify('SHA1'); + const verify = crypto.createVerify(dsaDigest); verify.update(input); assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); @@ -520,12 +566,18 @@ if (!process.features.openssl_is_boringssl) { // Test the legacy 'DSS1' name. const sign2 = crypto.createSign('DSS1'); sign2.update(input); - const signature2 = sign2.sign(dsaKeyPem, 'hex'); + if (fips30) { + assert.throws(() => sign2.sign(dsaKeyPem, 'hex'), { + code: fipsDigestErrorCode, + }); + } else { + const signature2 = sign2.sign(dsaKeyPem, 'hex'); - const verify2 = crypto.createVerify('DSS1'); - verify2.update(input); + const verify2 = crypto.createVerify('DSS1'); + verify2.update(input); - assert.strictEqual(verify2.verify(dsaPubPem, signature2, 'hex'), true); + assert.strictEqual(verify2.verify(dsaPubPem, signature2, 'hex'), true); + } } else { common.printSkipMessage('Skipping unsupported DSA test case'); } @@ -539,11 +591,12 @@ if (!process.features.openssl_is_boringssl) { // DSA signatures vary across runs so there is no static string to verify // against. - const sign = crypto.createSign('SHA1'); + const dsaDigest = fips3 ? 'SHA256' : 'SHA1'; + const sign = crypto.createSign(dsaDigest); sign.update(input); const signature = sign.sign(dsaPkcs8KeyPem, 'hex'); - const verify = crypto.createVerify('SHA1'); + const verify = crypto.createVerify(dsaDigest); verify.update(input); assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); @@ -558,22 +611,23 @@ if (!process.features.openssl_is_boringssl) { const input = 'I AM THE WALRUS'; { - const sign = crypto.createSign('SHA1'); + const sign = crypto.createSign(fips3 ? 'SHA256' : 'SHA1'); sign.update(input); assert.throws(() => { - sign.sign({ key: dsaKeyPemEncrypted, passphrase: 'wrong' }, 'hex'); + sign.sign({ key: dsaKeyPemEncrypted, passphrase: wrongPassphrase }, 'hex'); }, decryptPrivateKeyError); } if (!process.features.openssl_is_boringssl) { // DSA signatures vary across runs so there is no static string to verify // against. - const sign = crypto.createSign('SHA1'); + const dsaDigest = fips3 ? 'SHA256' : 'SHA1'; + const sign = crypto.createSign(dsaDigest); sign.update(input); const signOptions = { key: dsaKeyPemEncrypted, passphrase: 'password' }; const signature = sign.sign(signOptions, 'hex'); - const verify = crypto.createVerify('SHA1'); + const verify = crypto.createVerify(dsaDigest); verify.update(input); assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); diff --git a/test/parallel/test-crypto-scrypt.js b/test/parallel/test-crypto-scrypt.js index 421ee4ce8f34..3012a8fa55d5 100644 --- a/test/parallel/test-crypto-scrypt.js +++ b/test/parallel/test-crypto-scrypt.js @@ -5,6 +5,8 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); +const { hasFIPS } = require('../common/crypto'); +const isFips = hasFIPS(3); if (typeof crypto.scrypt !== 'function' || typeof crypto.scryptSync !== 'function') common.skip('no scrypt support'); @@ -159,13 +161,20 @@ const badargs = [ }, ]; -for (const options of good) { - const { pass, salt, keylen, expected } = options; - const actual = crypto.scryptSync(pass, salt, keylen, options); - assert.strictEqual(actual.toString('hex'), expected); - crypto.scrypt(pass, salt, keylen, options, common.mustSucceed((actual) => { +if (isFips) { + const expected = { code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS' }; + assert.throws(() => crypto.scryptSync('pass', 'salt', 1), expected); + assert.throws( + () => crypto.scrypt('pass', 'salt', 1, () => {}), expected); +} else { + for (const options of good) { + const { pass, salt, keylen, expected } = options; + const actual = crypto.scryptSync(pass, salt, keylen, options); assert.strictEqual(actual.toString('hex'), expected); - })); + crypto.scrypt(pass, salt, keylen, options, common.mustSucceed((actual) => { + assert.strictEqual(actual.toString('hex'), expected); + })); + } } for (const options of bad) { @@ -191,7 +200,9 @@ for (const options of incompatibleOptions) { } for (const options of toobig) { - const expected = { + const expected = isFips ? { + code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS', + } : { message: process.features.openssl_is_boringssl ? /Invalid scrypt params:.*(INVALID_PARAMETERS|MEMORY_LIMIT_EXCEEDED)/ : /Invalid scrypt params:.*memory limit exceeded/, @@ -203,7 +214,7 @@ for (const options of toobig) { expected); } -{ +if (!isFips) { const defaults = { N: 16384, p: 1, r: 8 }; const expected = crypto.scryptSync('pass', 'salt', 1, defaults); const actual = crypto.scryptSync('pass', 'salt', 1); @@ -229,10 +240,12 @@ for (const { args, expected } of badargs) { { // Values for maxmem that do not fit in 32 bits but that are still safe // integers should be allowed. - crypto.scrypt('', '', 4, { maxmem: 2 ** 52 }, - common.mustSucceed((actual) => { - assert.strictEqual(actual.toString('hex'), 'd72c87d0'); - })); + if (!isFips) { + crypto.scrypt('', '', 4, { maxmem: 2 ** 52 }, + common.mustSucceed((actual) => { + assert.strictEqual(actual.toString('hex'), 'd72c87d0'); + })); + } // Values that exceed Number.isSafeInteger should not be allowed. assert.throws(() => crypto.scryptSync('', '', 0, { maxmem: 2 ** 53 }), { @@ -240,7 +253,7 @@ for (const { args, expected } of badargs) { }); } -{ +if (!isFips) { // Regression test for https://github.com/nodejs/node/issues/28836. function testParameter(name, value) { @@ -299,5 +312,11 @@ for (const { args, expected } of badargs) { assert.deepStrictEqual(negResult, posResult); } - crypto.scrypt('', '', -0, common.mustCall()); + if (isFips) { + assert.throws( + () => crypto.scrypt('', '', -0, () => {}), + { code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS' }); + } else { + crypto.scrypt('', '', -0, common.mustCall()); + } } diff --git a/test/parallel/test-crypto-secure-heap.js b/test/parallel/test-crypto-secure-heap.js index ef686293b900..638ab49c82ca 100644 --- a/test/parallel/test-crypto-secure-heap.js +++ b/test/parallel/test-crypto-secure-heap.js @@ -20,7 +20,7 @@ if (process.features.openssl_is_boringssl) { const assert = require('assert'); const { fork } = require('child_process'); const fixtures = require('../common/fixtures'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); const { secureHeapUsed, createDiffieHellman, @@ -38,7 +38,8 @@ if (process.argv[2] === 'child') { assert.strictEqual(a.used, 0); { - const size = getFips() || hasOpenSSL3 ? 1024 : 256; + const size = hasFIPS(3) ? + 2048 : (getFips() === 1 || hasOpenSSL(3) ? 1024 : 256); const dh1 = createDiffieHellman(size); const p1 = dh1.getPrime('buffer'); const dh2 = createDiffieHellman(p1, 'buffer'); diff --git a/test/parallel/test-crypto-sign-verify.js b/test/parallel/test-crypto-sign-verify.js index 808c8d076a28..dfcafda08f84 100644 --- a/test/parallel/test-crypto-sign-verify.js +++ b/test/parallel/test-crypto-sign-verify.js @@ -10,6 +10,7 @@ const crypto = require('crypto'); const fixtures = require('../common/fixtures'); const { hasOpenSSL, + hasFIPS, opensslCli, } = require('../common/crypto'); @@ -17,6 +18,17 @@ const { const certPem = fixtures.readKey('rsa_cert.crt'); const keyPem = fixtures.readKey('rsa_private.pem'); const keySize = 2048; +const fips3 = hasFIPS(3); +const fips35 = hasFIPS(3, 5); +const fips30 = fips3 && !fips35; +const fipsDigestErrorCode = 'ERR_OSSL_DIGEST_NOT_ALLOWED'; +const signingHash = fips3 ? 'SHA256' : 'SHA1'; + +if (fips30) { + assert.throws( + () => crypto.createSign('SHA1').update('Test123').sign(keyPem), + { code: fipsDigestErrorCode }); +} { const Sign = crypto.Sign; @@ -60,7 +72,7 @@ const keySize = 2048; Object.defineProperty(Object.prototype, 'opensslErrorStack', errorStack); assert.throws(() => { - crypto.createSign('SHA1') + crypto.createSign('SHA256') .update('Test123') .sign({ key: keyPem, @@ -99,15 +111,15 @@ assert.throws( // Test signing and verifying { - const s1 = crypto.createSign('SHA1') + const s1 = crypto.createSign(signingHash) .update('Test123') .sign(keyPem, 'base64'); - let s1stream = crypto.createSign('SHA1'); + let s1stream = crypto.createSign(signingHash); s1stream.end('Test123'); s1stream = s1stream.sign(keyPem, 'base64'); assert.strictEqual(s1, s1stream, `${s1} should equal ${s1stream}`); - const verified = crypto.createVerify('SHA1') + const verified = crypto.createVerify(signingHash) .update('Test') .update('123') .verify(certPem, s1, 'base64'); @@ -138,16 +150,16 @@ assert.throws( } { - const s3 = crypto.createSign('SHA1') + const s3 = crypto.createSign(signingHash) .update('Test123') .sign(keyPem, 'buffer'); - let verified = crypto.createVerify('SHA1') + let verified = crypto.createVerify(signingHash) .update('Test') .update('123') .verify(certPem, s3); assert.strictEqual(verified, true); - const verStream = crypto.createVerify('SHA1'); + const verStream = crypto.createVerify(signingHash); verStream.write('Tes'); verStream.write('t12'); verStream.end('3'); @@ -190,6 +202,17 @@ assert.throws( const data = Buffer.from('Test123'); + if (fips30) { + const streamOptions = { + key: keyPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST, + }; + assert.throws( + () => crypto.createSign(algo).update(data).sign(streamOptions), + { code: fipsDigestErrorCode }); + } + signSaltLengths.forEach((signSaltLength) => { if (signSaltLength > max) { // If the salt length is too big, an Error should be thrown @@ -211,20 +234,23 @@ assert.throws( }, errMessage); } else { // Otherwise, a valid signature should be generated - const s4 = crypto.createSign(algo) - .update(data) - .sign({ - key: keyPem, - padding: crypto.constants.RSA_PKCS1_PSS_PADDING, - saltLength: signSaltLength - }); const s4_2 = crypto.sign(algo, data, { key: keyPem, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: signSaltLength }); + const signatures = [s4_2]; + if (!fips30) { + signatures.unshift(crypto.createSign(algo) + .update(data) + .sign({ + key: keyPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: signSaltLength + })); + } - [s4, s4_2].forEach((sig) => { + signatures.forEach((sig) => { let verified; verifySaltLengths.forEach((verifySaltLength) => { // Verification should succeed if and only if the salt length is @@ -281,7 +307,8 @@ assert.throws( }); } - testPSS('SHA1', 20); + if (!fips30) + testPSS('SHA1', 20); testPSS('SHA256', 32); } @@ -340,7 +367,7 @@ assert.throws( }); assert.throws(() => { - crypto.createSign('SHA1') + crypto.createSign('SHA256') .update('Test123') .sign({ key: keyPem, @@ -365,7 +392,7 @@ assert.throws( // Test throws exception when key options is null { assert.throws(() => { - crypto.createSign('SHA1').update('Test123').sign(null, 'base64'); + crypto.createSign('SHA256').update('Test123').sign(null, 'base64'); }, { code: 'ERR_CRYPTO_SIGN_KEY_REQUIRED', name: 'Error' @@ -373,8 +400,8 @@ assert.throws( } { - const sign = crypto.createSign('SHA1'); - const verify = crypto.createVerify('SHA1'); + const sign = crypto.createSign('SHA256'); + const verify = crypto.createVerify('SHA256'); [1, [], {}, undefined, null, true, Infinity].forEach((input) => { const errObj = { @@ -441,7 +468,7 @@ for (const pair of [ { private: fixtures.readKey('rsa_private_2048.pem', 'ascii'), public: fixtures.readKey('rsa_public_2048.pem', 'ascii'), skip: false, - algo: 'sha1', + algo: signingHash, sigLen: 256, raw: false }, ]) { @@ -450,6 +477,7 @@ for (const pair of [ continue; } const algo = pair.algo; + const keyType = crypto.createPrivateKey(pair.private).asymmetricKeyType; { const data = Buffer.from('Hello world'); @@ -521,15 +549,34 @@ for (const pair of [ const sig = crypto.sign(algo, data, { key: pair.private, context }); assert.strictEqual(crypto.verify(algo, data, { key: pair.public }, sig), true); assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context }, sig), true); - assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context: crypto.randomBytes(30) }, sig), false); + const mismatchedContext = { key: pair.public, context: crypto.randomBytes(30) }; + if (fips35 && keyType === 'ed25519') { + assert.throws(() => crypto.verify(algo, data, mismatchedContext, sig), { + code: 'ERR_OSSL_INVALID_EDDSA_INSTANCE_FOR_ATTEMPTED_OPERATION', + }); + } else { + assert.strictEqual( + crypto.verify(algo, data, mismatchedContext, sig), false); + } } { const context = new Uint8Array(32); - const sig = crypto.sign(algo, data, { key: pair.private, context }); - assert.strictEqual(crypto.verify(algo, data, { key: pair.public }, sig), false); - assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context }, sig), true); - assert.strictEqual(crypto.verify(algo, data, { key: pair.public, context: crypto.randomBytes(30) }, sig), false); + if (fips35 && keyType === 'ed25519') { + assert.throws( + () => crypto.sign(algo, data, { key: pair.private, context }), + { code: 'ERR_OSSL_INVALID_EDDSA_INSTANCE_FOR_ATTEMPTED_OPERATION' }); + } else { + const sig = crypto.sign(algo, data, { key: pair.private, context }); + assert.strictEqual( + crypto.verify(algo, data, { key: pair.public }, sig), false); + assert.strictEqual( + crypto.verify(algo, data, { key: pair.public, context }, sig), true); + assert.strictEqual(crypto.verify(algo, data, { + key: pair.public, + context: crypto.randomBytes(30), + }, sig), false); + } } assert.throws(() => crypto.sign(algo, data, { key: pair.private, context: new Uint8Array(256) }), { @@ -572,20 +619,27 @@ if (hasOpenSSL(3, 2)) { { const context = Buffer.from('my context'); - const sig = crypto.sign(null, data, { key: privKey, context }); - assert.strictEqual(sig.length, 64); + if (fips35) { + assert.throws(() => crypto.sign(null, data, { key: privKey, context }), { + code: 'ERR_OSSL_INVALID_EDDSA_INSTANCE_FOR_ATTEMPTED_OPERATION', + }); + } else { + const sig = crypto.sign(null, data, { key: privKey, context }); + assert.strictEqual(sig.length, 64); - // Verify with matching context succeeds - assert.strictEqual(crypto.verify(null, data, { key: pubKey, context }, sig), true); + // Verify with matching context succeeds + assert.strictEqual( + crypto.verify(null, data, { key: pubKey, context }, sig), true); - // Verify without context fails (Ed25519ctx !== Ed25519 pure) - assert.strictEqual(crypto.verify(null, data, { key: pubKey }, sig), false); + // Verify without context fails (Ed25519ctx !== Ed25519 pure) + assert.strictEqual(crypto.verify(null, data, { key: pubKey }, sig), false); - // Verify with wrong context fails - assert.strictEqual(crypto.verify(null, data, { - key: pubKey, - context: Buffer.from('wrong'), - }, sig), false); + // Verify with wrong context fails + assert.strictEqual(crypto.verify(null, data, { + key: pubKey, + context: Buffer.from('wrong'), + }, sig), false); + } } { @@ -662,7 +716,9 @@ MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE1fiOx1BhdoAvpolZdyX46aGWlNoa { const data = Buffer.from('Hello world'); - const keys = [['ec-key.pem', 64], ['dsa_private_1025.pem', 40]]; + const dsaKey = fips3 ? + ['dsa_private.pem', 64] : ['dsa_private_1025.pem', 40]; + const keys = [['ec-key.pem', 64], dsaKey]; for (const [file, length] of keys) { if (process.features.openssl_is_boringssl && file.startsWith('dsa_')) { @@ -670,28 +726,29 @@ MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE1fiOx1BhdoAvpolZdyX46aGWlNoa continue; } const privKey = fixtures.readKey(file); + const digest = fips3 ? 'sha256' : 'sha1'; [ - crypto.createSign('sha1').update(data).sign(privKey), - crypto.sign('sha1', data, privKey), - crypto.sign('sha1', data, { key: privKey, dsaEncoding: 'der' }), + crypto.createSign(digest).update(data).sign(privKey), + crypto.sign(digest, data, privKey), + crypto.sign(digest, data, { key: privKey, dsaEncoding: 'der' }), ].forEach((sig) => { // Signature length variability due to DER encoding assert(sig.length >= length + 4 && sig.length <= length + 8); assert.strictEqual( - crypto.createVerify('sha1').update(data).verify(privKey, sig), + crypto.createVerify(digest).update(data).verify(privKey, sig), true ); - assert.strictEqual(crypto.verify('sha1', data, privKey, sig), true); + assert.strictEqual(crypto.verify(digest, data, privKey, sig), true); }); // Test (EC)DSA signature conversion. const opts = { key: privKey, dsaEncoding: 'ieee-p1363' }; - let sig = crypto.sign('sha1', data, opts); + let sig = crypto.sign(digest, data, opts); // Unlike DER signatures, IEEE P1363 signatures have a predictable length. assert.strictEqual(sig.length, length); - assert.strictEqual(crypto.verify('sha1', data, opts, sig), true); - assert.strictEqual(crypto.createVerify('sha1') + assert.strictEqual(crypto.verify(digest, data, opts, sig), true); + assert.strictEqual(crypto.createVerify(digest) .update(data) .verify(opts, sig), true); @@ -700,7 +757,7 @@ MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE1fiOx1BhdoAvpolZdyX46aGWlNoa sig = crypto.randomBytes(length + i); let result; try { - result = crypto.verify('sha1', data, opts, sig); + result = crypto.verify(digest, data, opts, sig); } catch (err) { assert.match(err.message, /asn1 encoding/); assert.strictEqual(err.library, 'asn1 encoding routines'); @@ -735,20 +792,20 @@ MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE1fiOx1BhdoAvpolZdyX46aGWlNoa } // Non-(EC)DSA keys should ignore the option. - const sig = crypto.sign('sha1', data, { + const sig = crypto.sign(signingHash, data, { key: keyPem, dsaEncoding: 'ieee-p1363' }); - assert.strictEqual(crypto.verify('sha1', data, certPem, sig), true); + assert.strictEqual(crypto.verify(signingHash, data, certPem, sig), true); assert.strictEqual( - crypto.verify('sha1', data, { + crypto.verify(signingHash, data, { key: certPem, dsaEncoding: 'ieee-p1363' }, sig), true ); assert.strictEqual( - crypto.verify('sha1', data, { + crypto.verify(signingHash, data, { key: certPem, dsaEncoding: 'der' }, sig), @@ -757,7 +814,7 @@ MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE1fiOx1BhdoAvpolZdyX46aGWlNoa for (const dsaEncoding of ['foo', null, {}, 5, true, NaN]) { assert.throws(() => { - crypto.sign('sha1', data, { + crypto.sign(signingHash, data, { key: certPem, dsaEncoding }); @@ -776,12 +833,13 @@ if (!opensslCli) { const privkey = fixtures.readKey('rsa_private_2048.pem'); const msg = 'Test123'; - const s5 = crypto.createSign('SHA256') - .update(msg) - .sign({ - key: privkey, - padding: crypto.constants.RSA_PKCS1_PSS_PADDING - }); + const options = { + key: privkey, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + }; + const s5 = fips30 ? + crypto.sign('SHA256', Buffer.from(msg), options) : + crypto.createSign('SHA256').update(msg).sign(options); const tmpdir = require('../common/tmpdir'); tmpdir.refresh(); @@ -811,8 +869,14 @@ if (!process.features.openssl_is_boringssl) { const privateKey = crypto.createPrivateKey(privatePem); for (const key of [privatePem, privateKey]) { - // Any algorithm should work. - for (const algo of ['sha1', 'sha256']) { + if (fips30) { + assert.throws(() => crypto.sign('sha1', 'foo', key), { + code: fipsDigestErrorCode, + }); + } + // Any algorithm should work unless SHA-1 signing is unavailable. + const algorithms = fips30 ? ['sha256'] : ['sha1', 'sha256']; + for (const algo of algorithms) { // Any salt length should work. for (const saltLength of [undefined, 8, 10, 12, 16, 18, 20]) { const signature = crypto.sign(algo, 'foo', { key, saltLength }); @@ -847,7 +911,9 @@ if (!process.features.openssl_is_boringssl) { // Signing with anything other than sha256 should fail. assert.throws(() => { crypto.sign('sha1', 'foo', key); - }, /digest not allowed/); + }, fips30 ? { + code: fipsDigestErrorCode, + } : /digest not allowed/); // Signing with salt lengths less than 16 bytes should fail. for (const saltLength of [8, 10, 12]) { @@ -895,7 +961,9 @@ if (!process.features.openssl_is_boringssl) { for (const algo of ['sha1', 'sha256']) { assert.throws(() => { crypto.sign(algo, 'foo', key); - }, /digest not allowed/); + }, fips30 && algo === 'sha1' ? { + code: fipsDigestErrorCode, + } : /digest not allowed/); } // sha512 should produce a valid signature. @@ -915,15 +983,24 @@ if (!process.features.openssl_is_boringssl) { // The sign function should not swallow OpenSSL errors. // Regression test for https://github.com/nodejs/node/issues/40794. { - assert.throws(() => { - const { privateKey } = crypto.generateKeyPairSync('rsa', { - modulusLength: 512 + if (fips3) { + assert.throws(() => crypto.generateKeyPairSync('rsa', { + modulusLength: 512, + }), { + name: 'Error', + message: 'error:020000AE:rsa routines::invalid modulus', }); - crypto.sign('sha512', 'message', privateKey); - }, { - code: 'ERR_OSSL_RSA_DIGEST_TOO_BIG_FOR_RSA_KEY', - message: /digest too big for rsa key|DIGEST_TOO_BIG_FOR_RSA_KEY/ - }); + } else { + assert.throws(() => { + const { privateKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 512 + }); + crypto.sign('sha512', 'message', privateKey); + }, { + code: 'ERR_OSSL_RSA_DIGEST_TOO_BIG_FOR_RSA_KEY', + message: /digest too big for rsa key|DIGEST_TOO_BIG_FOR_RSA_KEY/ + }); + } } { diff --git a/test/parallel/test-crypto-worker-thread.js b/test/parallel/test-crypto-worker-thread.js index d9030d5cfc11..42027f3f2b44 100644 --- a/test/parallel/test-crypto-worker-thread.js +++ b/test/parallel/test-crypto-worker-thread.js @@ -9,6 +9,7 @@ const { generateKeySync, generateKeyPairSync, } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; const assert = require('assert'); @@ -19,7 +20,7 @@ if (isMainThread) { (async () => { const secretKey = generateKeySync('aes', { length: 128 }); const { publicKey, privateKey } = generateKeyPairSync('rsa', { - modulusLength: 1024 + modulusLength: hasFIPS(3) ? 2048 : 1024 }); const cryptoKey = await subtle.generateKey( { name: 'AES-CBC', length: 128 }, false, ['encrypt']); diff --git a/test/parallel/test-crypto.js b/test/parallel/test-crypto.js index 46f4571b33df..2379ea692cac 100644 --- a/test/parallel/test-crypto.js +++ b/test/parallel/test-crypto.js @@ -29,7 +29,8 @@ const assert = require('assert'); const crypto = require('crypto'); const tls = require('tls'); const fixtures = require('../common/fixtures'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); +const isFips = hasFIPS(3); // Test Certificates const certPfx = fixtures.readKey('rsa_cert.pfx'); @@ -54,27 +55,35 @@ assert.throws(() => { }); // PFX tests -tls.createSecureContext({ pfx: certPfx, passphrase: 'sample' }); - -assert.throws(() => { - tls.createSecureContext({ pfx: certPfx }); -}, (err) => { - // Throws general Error, so there is no opensslErrorStack property. - return err instanceof Error && - err.name === 'Error' && - /^Error: (mac verify failure|INCORRECT_PASSWORD)$/.test(err) && - !('opensslErrorStack' in err); -}); +if (isFips) { + for (const passphrase of ['sample', undefined, 'test']) { + assert.throws( + () => tls.createSecureContext({ pfx: certPfx, passphrase }), + { code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION' }); + } +} else { + tls.createSecureContext({ pfx: certPfx, passphrase: 'sample' }); + + assert.throws(() => { + tls.createSecureContext({ pfx: certPfx }); + }, (err) => { + // Throws general Error, so there is no opensslErrorStack property. + return err instanceof Error && + err.name === 'Error' && + /^Error: (mac verify failure|INCORRECT_PASSWORD)$/.test(err) && + !('opensslErrorStack' in err); + }); -assert.throws(() => { - tls.createSecureContext({ pfx: certPfx, passphrase: 'test' }); -}, (err) => { - // Throws general Error, so there is no opensslErrorStack property. - return err instanceof Error && - err.name === 'Error' && - /^Error: (mac verify failure|INCORRECT_PASSWORD)$/.test(err) && - !('opensslErrorStack' in err); -}); + assert.throws(() => { + tls.createSecureContext({ pfx: certPfx, passphrase: 'test' }); + }, (err) => { + // Throws general Error, so there is no opensslErrorStack property. + return err instanceof Error && + err.name === 'Error' && + /^Error: (mac verify failure|INCORRECT_PASSWORD)$/.test(err) && + !('opensslErrorStack' in err); + }); +} assert.throws(() => { tls.createSecureContext({ pfx: 'sample', passphrase: 'test' }); @@ -191,7 +200,7 @@ assert.throws( ); assert.throws( - () => crypto.createHmac('sha256', 'a secret').update('0', 'hex'), + () => crypto.createHmac('sha256', '0123456789abcdef').update('0', 'hex'), (error) => { assert.ok(!('opensslErrorStack' in error)); assert.throws(() => { throw error; }, encodingError); @@ -211,7 +220,11 @@ assert.throws(() => { ].join('\n'); crypto.createSign('SHA256').update('test').sign(priv); }, (err) => { - if (process.features.openssl_is_boringssl) { + if (isFips) { + assert.throws(() => { throw err; }, { + code: 'ERR_OSSL_INVALID_KEY_LENGTH', + }); + } else if (process.features.openssl_is_boringssl) { // BoringSSL rejects the tiny RSA key while decoding it, before signing. assert.throws(() => { throw err; }, { name: 'Error', @@ -225,9 +238,9 @@ assert.throws(() => { assert(Array.isArray(err.opensslErrorStack)); assert(err.opensslErrorStack.length > 0); } else { - if (!hasOpenSSL3) + if (!hasOpenSSL(3)) assert.ok(!('opensslErrorStack' in err)); - assert.throws(() => { throw err; }, hasOpenSSL3 ? { + assert.throws(() => { throw err; }, hasOpenSSL(3) ? { name: 'Error', message: 'error:02000070:rsa routines::digest too big for rsa key', library: 'rsa routines', @@ -243,7 +256,7 @@ assert.throws(() => { return true; }); -if (!hasOpenSSL3) { +if (!hasOpenSSL(3)) { // The correct header inside `rsa_private_pkcs8_bad.pem` should have been // -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY----- // instead of diff --git a/test/parallel/test-https-agent-additional-options.js b/test/parallel/test-https-agent-additional-options.js index 000cb9d3d0c2..3707855ed5c8 100644 --- a/test/parallel/test-https-agent-additional-options.js +++ b/test/parallel/test-https-agent-additional-options.js @@ -6,17 +6,20 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); const https = require('https'); +const { hasFIPS } = require('../common/crypto'); const fixtures = require('../common/fixtures'); +const fips3 = hasFIPS(3); const options = { key: fixtures.readKey('agent1-key.pem'), cert: fixtures.readKey('agent1-cert.pem'), ca: fixtures.readKey('ca1-cert.pem'), - minVersion: 'TLSv1.1', + minVersion: fips3 ? 'TLSv1.2' : 'TLSv1.1', }; if (!process.features.openssl_is_boringssl) { - options.ciphers = 'ALL@SECLEVEL=0'; + options.ciphers = fips3 ? + 'ECDHE-RSA-AES256-GCM-SHA384' : 'ALL@SECLEVEL=0'; } const server = https.Server(options, (req, res) => { @@ -34,7 +37,8 @@ function getBaseOptions(port) { }; if (!process.features.openssl_is_boringssl) { - baseOptions.ciphers = 'ALL@SECLEVEL=0'; + baseOptions.ciphers = fips3 ? + 'ECDHE-RSA-AES256-GCM-SHA384' : 'ALL@SECLEVEL=0'; } return baseOptions; @@ -44,10 +48,11 @@ const updatedValues = new Map([ ['dhparam', fixtures.readKey('dh2048.pem')], ['ecdhCurve', 'secp384r1'], ['honorCipherOrder', true], - ['minVersion', 'TLSv1.1'], + ['minVersion', fips3 ? 'TLSv1.2' : 'TLSv1.1'], ['maxVersion', 'TLSv1.3'], ['secureOptions', crypto.constants.SSL_OP_CIPHER_SERVER_PREFERENCE], - ['secureProtocol', 'TLSv1_1_method'], + ['secureProtocol', fips3 ? + 'TLSv1_2_method' : 'TLSv1_1_method'], ['sessionIdContext', 'sessionIdContext'], ]); diff --git a/test/parallel/test-https-agent-pfx-object-array-reuse.js b/test/parallel/test-https-agent-pfx-object-array-reuse.js index 95134855e971..758c32e54008 100644 --- a/test/parallel/test-https-agent-pfx-object-array-reuse.js +++ b/test/parallel/test-https-agent-pfx-object-array-reuse.js @@ -6,21 +6,57 @@ if (!common.hasCrypto) const assert = require('assert'); const https = require('https'); +const { hasFIPS } = require('../common/crypto'); const fixtures = require('../common/fixtures'); +const fips3 = hasFIPS(3); +const fips35 = hasFIPS(3, 5); + +const onRequest = (req, res) => { + res.end(req.socket.getPeerCertificate().subject.CN); +}; +const requestHandler = fips3 && !fips35 ? + common.mustNotCall() : + common.mustCall(onRequest, fips3 ? 1 : 2); const server = https.createServer({ key: fixtures.readKey('agent2-key.pem'), cert: fixtures.readKey('agent2-cert.pem'), requestCert: true, rejectUnauthorized: false, -}, common.mustCall((req, res) => { - res.end(req.socket.getPeerCertificate().subject.CN); -}, 2)); +}, requestHandler); server.listen(0, common.mustCall(async () => { const agent = new https.Agent({ keepAlive: true, maxSockets: 1 }); const port = server.address().port; + if (fips3) { + await assert.rejects(request({ + agent, + port, + pfx: [{ buf: fixtures.readKey('agent1.pfx'), passphrase: 'sample' }], + }, false), { code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION' }); + + if (!fips35) { + agent.destroy(); + server.close(); + return; + } + + const result = await request({ + agent, + port, + pfx: [{ + buf: fixtures.readKey('agent1-fips.pfx'), + passphrase: 'password', + }], + }); + assert.strictEqual(result.body, 'agent1'); + assert.strictEqual(result.reusedSocket, false); + agent.destroy(); + server.close(); + return; + } + const first = await request({ agent, port, @@ -41,19 +77,20 @@ server.listen(0, common.mustCall(async () => { server.close(); })); -function request(options) { +function request(options, expectResponse = true) { return new Promise((resolve, reject) => { - const req = https.get({ - ...options, - rejectUnauthorized: false, - }, common.mustCall((res) => { + const onResponse = expectResponse ? common.mustCall((res) => { let body = ''; res.setEncoding('utf8'); res.on('data', (chunk) => body += chunk); res.on('end', common.mustCall(() => { resolve({ body, reusedSocket: req.reusedSocket }); })); - })); + }) : common.mustNotCall(); + const req = https.get({ + ...options, + rejectUnauthorized: false, + }, onResponse); req.on('error', reject); }); } diff --git a/test/parallel/test-https-agent-session-eviction.js b/test/parallel/test-https-agent-session-eviction.js index de2790e0d0a3..971a8f359a6f 100644 --- a/test/parallel/test-https-agent-session-eviction.js +++ b/test/parallel/test-https-agent-session-eviction.js @@ -8,10 +8,11 @@ if (!common.hasCrypto) { } const fixtures = require('../common/fixtures'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); const https = require('https'); -const { SSL_OP_NO_TICKET } = require('crypto').constants; +const { constants: { SSL_OP_NO_TICKET } } = require('crypto'); +const fips3 = hasFIPS(3); const options = { key: fixtures.readKey('agent1-key.pem'), @@ -19,11 +20,17 @@ const options = { secureOptions: SSL_OP_NO_TICKET, }; +if (fips3) { + options.minVersion = 'TLSv1.3'; + options.maxVersion = 'TLSv1.3'; +} + if (!process.features.openssl_is_boringssl) { - options.ciphers = 'RSA@SECLEVEL=0'; + options.ciphers = fips3 ? + 'ECDHE-RSA-AES256-GCM-SHA384' : 'RSA@SECLEVEL=0'; } -// Create TLS1.2 server +// Create the initial server and cache a session from it. https.createServer(options, function(req, res) { res.writeHead(200, { 'Connection': 'close' }); res.end('ohai'); @@ -47,9 +54,14 @@ function first(server) { req.end(); } -// Create TLS1 server +// Create a server constrained to a different TLS version. function faultyServer(port) { - options.secureProtocol = 'TLSv1_method'; + if (fips3) { + options.minVersion = 'TLSv1.2'; + options.maxVersion = 'TLSv1.2'; + } else { + options.secureProtocol = 'TLSv1_method'; + } https.createServer(options, function(req, res) { res.writeHead(200, { 'Connection': 'close' }); res.end('hello faulty'); @@ -62,14 +74,15 @@ function faultyServer(port) { function second(server, session) { const req = https.request({ port: server.address().port, - ciphers: (hasOpenSSL(3, 1) ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), + ciphers: fips3 ? 'ECDHE-RSA-AES256-GCM-SHA384' : + (hasOpenSSL(3, 1) ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), rejectUnauthorized: false }, function(res) { res.resume(); }); - // Although we have a TLS 1.2 session to offer to the TLS 1.0 server, - // connection to the TLS 1.0 server should work. + // Offering the cached session to a server using another TLS version should + // not prevent a fresh connection. req.on('response', common.mustCall(function(res) { // The test is now complete for OpenSSL 1.1.0. server.close(); diff --git a/test/parallel/test-https-pfx.js b/test/parallel/test-https-pfx.js index 3c0aa82b3dbe..2be91887207d 100644 --- a/test/parallel/test-https-pfx.js +++ b/test/parallel/test-https-pfx.js @@ -29,6 +29,8 @@ const fixtures = require('../common/fixtures'); const assert = require('assert'); const https = require('https'); +const { hasFIPS } = require('../common/crypto'); +const fips3 = hasFIPS(3); const pfx = fixtures.readKey('rsa_cert.pfx'); @@ -42,10 +44,23 @@ const options = { rejectUnauthorized: false }; +if (fips3) { + assert.throws(() => https.createServer(options), { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + }); + + if (!hasFIPS(3, 5)) { + return; + } + + options.pfx = fixtures.readKey('agent1-fips.pfx'); + options.passphrase = 'password'; +} + const server = https.createServer(options, common.mustCallAtLeast((req, res) => { - assert.strictEqual(req.socket.authorized, false); // not a client cert - assert.strictEqual(req.socket.authorizationError, - 'DEPTH_ZERO_SELF_SIGNED_CERT'); + assert.strictEqual(req.socket.authorized, fips3); + assert.strictEqual(req.socket.authorizationError, fips3 ? + null : 'DEPTH_ZERO_SELF_SIGNED_CERT'); res.writeHead(200); res.end('OK'); })); diff --git a/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.js b/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.js index 667be03d58ce..ec1b8dda8ca1 100644 --- a/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.js +++ b/test/parallel/test-https-selfsigned-no-keycertsign-no-crash.js @@ -13,9 +13,10 @@ if (!common.hasCrypto) common.skip('missing crypto'); const crypto = require('crypto'); +const { hasOpenSSL } = require('../common/crypto'); // See #37990 for details on why this is problematic with FIPS. -if (process.config.variables.openssl_is_fips) +if (crypto.getFips() === 1 && !hasOpenSSL(3)) common.skip('Skipping as test uses non-fips compliant EC curve'); // This test will fail for OpenSSL < 1.1.1h diff --git a/test/parallel/test-tls-alert.js b/test/parallel/test-tls-alert.js index 64b7080e39ba..f20bf42a2dcd 100644 --- a/test/parallel/test-tls-alert.js +++ b/test/parallel/test-tls-alert.js @@ -27,6 +27,7 @@ if (!common.hasCrypto) { const { hasOpenSSL, + hasFIPS, opensslCli, } = require('../common/crypto'); @@ -43,11 +44,18 @@ function loadPEM(n) { return fixtures.readKey(`${n}.pem`); } -const server = tls.Server({ - secureProtocol: 'TLSv1_2_server_method', +const serverOptions = { key: loadPEM('agent2-key'), - cert: loadPEM('agent2-cert') -}, null).listen(0, common.mustCall(() => { + cert: loadPEM('agent2-cert'), +}; +if (hasFIPS(3)) { + serverOptions.minVersion = 'TLSv1.3'; + serverOptions.maxVersion = 'TLSv1.3'; +} else { + serverOptions.secureProtocol = 'TLSv1_2_server_method'; +} + +const server = tls.Server(serverOptions, null).listen(0, common.mustCall(() => { if (process.features.openssl_is_boringssl) { let gotClientError = false; let gotServerError = false; @@ -75,8 +83,9 @@ const server = tls.Server({ return; } - const args = ['s_client', '-quiet', '-tls1_1', - '-cipher', (hasOpenSSL(3, 1) ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), + const args = ['s_client', '-quiet', hasFIPS(3) ? '-tls1_2' : '-tls1_1', + '-cipher', hasFIPS(3) ? 'DEFAULT' : + (hasOpenSSL(3, 1) ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), '-connect', `127.0.0.1:${server.address().port}`]; execFile(opensslCli, args, common.mustCall((err, _, stderr) => { diff --git a/test/parallel/test-tls-client-getephemeralkeyinfo.js b/test/parallel/test-tls-client-getephemeralkeyinfo.js index 82572c4e4975..638df3ff0141 100644 --- a/test/parallel/test-tls-client-getephemeralkeyinfo.js +++ b/test/parallel/test-tls-client-getephemeralkeyinfo.js @@ -9,7 +9,7 @@ if (process.features.openssl_is_boringssl) { } const fixtures = require('../common/fixtures'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); const assert = require('assert'); const { X509Certificate } = require('crypto'); @@ -17,12 +17,14 @@ const tls = require('tls'); const key = fixtures.readKey('agent2-key.pem'); const cert = fixtures.readKey('agent2-cert.pem'); +const fips3 = hasFIPS(3); +const rejectsXCurves = hasFIPS(3, 5); function loadDHParam(n) { return fixtures.readKey(`dh${n}.pem`); } -function test(size, type, name, cipher) { +function test(size, type, name, cipher, expectError = false) { assert(cipher); const options = { @@ -49,39 +51,62 @@ function test(size, type, name, cipher) { } } - const server = tls.createServer(options, common.mustCall((conn) => { - assert.strictEqual(conn.getEphemeralKeyInfo(), null); - conn.end(); - })); + if (rejectsXCurves && (name === 'X25519' || name === 'X448')) { + assert.throws(() => tls.createServer(options), { + code: 'ERR_CRYPTO_OPERATION_FAILED', + }); + return; + } + + const onConnection = expectError ? common.mustNotCall() : + common.mustCall((conn) => { + assert.strictEqual(conn.getEphemeralKeyInfo(), null); + conn.end(); + }); + const server = tls.createServer(options, onConnection); server.on('close', common.mustSucceed()); server.listen(0, common.mustCall(() => { + const onSecureConnect = expectError ? common.mustNotCall() : + common.mustCall(function() { + const ekeyinfo = client.getEphemeralKeyInfo(); + assert.strictEqual(ekeyinfo.type, type); + assert.strictEqual(ekeyinfo.size, size); + assert.strictEqual(ekeyinfo.name, name); + server.close(); + }); const client = tls.connect({ port: server.address().port, rejectUnauthorized: false - }, common.mustCall(function() { - const ekeyinfo = client.getEphemeralKeyInfo(); - assert.strictEqual(ekeyinfo.type, type); - assert.strictEqual(ekeyinfo.size, size); - assert.strictEqual(ekeyinfo.name, name); - server.close(); - })); - client.on('secureConnect', common.mustCall()); + }, onSecureConnect); + if (expectError) { + client.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_BAD_DH_VALUE'); + server.close(); + })); + } else { + client.on('secureConnect', common.mustCall()); + } })); } -test(undefined, undefined, undefined, 'AES256-SHA256'); +if (!fips3) + test(undefined, undefined, undefined, 'AES256-SHA256'); test('auto', 'DH', undefined, 'DHE-RSA-AES256-GCM-SHA384'); -if (hasOpenSSL(4, 0)) { - // OpenSSL 4.0 implements RFC 7919 FFDHE negotiation for TLS 1.2 and - // always selects FFDHE-2048 regardless of the server-supplied dhparam. -} else if (!hasOpenSSL(3, 2)) { - test(1024, 'DH', undefined, 'DHE-RSA-AES256-GCM-SHA384'); +if (fips3 && !hasOpenSSL(4)) { + test(2048, 'DH', undefined, 'DHE-RSA-AES256-GCM-SHA384', true); } else { - test(3072, 'DH', undefined, 'DHE-RSA-AES256-GCM-SHA384'); + if (hasOpenSSL(4, 0)) { + // OpenSSL 4.0 implements RFC 7919 FFDHE negotiation for TLS 1.2 and + // always selects FFDHE-2048 regardless of the server-supplied dhparam. + } else if (!hasOpenSSL(3, 2)) { + test(1024, 'DH', undefined, 'DHE-RSA-AES256-GCM-SHA384'); + } else { + test(3072, 'DH', undefined, 'DHE-RSA-AES256-GCM-SHA384'); + } + test(2048, 'DH', undefined, 'DHE-RSA-AES256-GCM-SHA384'); } -test(2048, 'DH', undefined, 'DHE-RSA-AES256-GCM-SHA384'); test(256, 'ECDH', 'prime256v1', 'ECDHE-RSA-AES256-GCM-SHA384'); test(521, 'ECDH', 'secp521r1', 'ECDHE-RSA-AES256-GCM-SHA384'); test(253, 'ECDH', 'X25519', 'ECDHE-RSA-AES256-GCM-SHA384'); @@ -96,6 +121,17 @@ function testTLS13Group(size, type, name) { maxVersion: 'TLSv1.3', }; + const unsupportedFipsGroup = + (rejectsXCurves && name === 'X25519') || + (hasFIPS(4) && + (name === 'curveSM2' || name === 'curveSM2MLKEM768')); + if (unsupportedFipsGroup) { + assert.throws(() => tls.createServer(options), { + code: 'ERR_CRYPTO_OPERATION_FAILED', + }); + return; + } + const server = tls.createServer(options, common.mustCall((conn) => { assert.strictEqual(conn.getEphemeralKeyInfo(), null); conn.end(); @@ -121,6 +157,8 @@ function testTLS13Group(size, type, name) { })); } +if (fips3) + testTLS13Group(256, 'ECDH', 'prime256v1'); testTLS13Group(253, 'ECDH', 'X25519'); if (hasOpenSSL(3, 5)) { diff --git a/test/parallel/test-tls-client-mindhsize.js b/test/parallel/test-tls-client-mindhsize.js index d777a9bfa97f..8f3b2eafbb8a 100644 --- a/test/parallel/test-tls-client-mindhsize.js +++ b/test/parallel/test-tls-client-mindhsize.js @@ -13,7 +13,9 @@ const secLevel = require('internal/crypto/util').getOpenSSLSecLevel(); const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); +const fips3 = hasFIPS(3); +const fips4 = hasFIPS(4); const key = fixtures.readKey('agent2-key.pem'); const cert = fixtures.readKey('agent2-cert.pem'); @@ -29,7 +31,7 @@ function test(size, err, next, minDHSizeOverride) { const options = { key: key, cert: cert, - dhparam: loadDHParam(size), + dhparam: size === 'auto' ? 'auto' : loadDHParam(size), ciphers: 'DHE-RSA-AES128-GCM-SHA256' }; @@ -60,7 +62,8 @@ function test(size, err, next, minDHSizeOverride) { if (err) { client.on('error', common.mustCall((e) => { nerror++; - assert.strictEqual(e.code, 'ERR_TLS_DH_PARAM_SIZE'); + assert.strictEqual(e.code, fips3 && !fips4 ? + 'ERR_SSL_BAD_DH_VALUE' : 'ERR_TLS_DH_PARAM_SIZE'); server.close(); })); } @@ -86,7 +89,11 @@ function testDHE3072() { } if (!process.features.openssl_is_boringssl) { - if (hasOpenSSL(4, 0)) { + if (fips3 && !fips4) { + // The FIPS provider rejects explicit DH parameters without a validated + // subgroup, while OpenSSL's built-in FFDHE group remains available. + testDHE2048(true, () => test('auto', false, null, 2048)); + } else if (hasOpenSSL(4, 0)) { // OpenSSL 4.0 implements RFC 7919 FFDHE negotiation for TLS 1.2 and // ignores the server-supplied dhparam in favor of FFDHE-2048. The 3072 // success case is therefore replaced by a 2048 success case. diff --git a/test/parallel/test-tls-dhe.js b/test/parallel/test-tls-dhe.js index 83af6daccbd0..65f3dc6867c4 100644 --- a/test/parallel/test-tls-dhe.js +++ b/test/parallel/test-tls-dhe.js @@ -34,6 +34,7 @@ if (process.features.openssl_is_boringssl) { const { opensslCli, hasOpenSSL, + hasFIPS, } = require('../common/crypto'); // OpenSSL has a set of security levels which affect what algorithms @@ -62,7 +63,7 @@ const dheCipher = 'DHE-RSA-AES128-SHA256'; const ecdheCipher = 'ECDHE-RSA-AES128-SHA256'; const ciphers = `${dheCipher}:${ecdheCipher}`; -if (secLevel < 2) { +if (secLevel < 2 && !hasFIPS(3)) { // Test will emit a warning because the DH parameter size is < 2048 bits // when the test is run on versions lower than OpenSSL32 common.expectWarning('SecurityWarning', @@ -74,7 +75,7 @@ function loadDHParam(n) { return fixtures.readKey(keyname); } -function test(dhparam, keylen, expectedCipher) { +function test(dhparam, keylen, expectedCipher, expectedError) { const options = { key, cert, @@ -84,12 +85,29 @@ function test(dhparam, keylen, expectedCipher) { }; const server = tls.createServer(options, (conn) => conn.end()); + if (typeof expectedError === 'string' || Array.isArray(expectedError)) { + server.once('tlsClientError', common.mustCall((err) => { + if (Array.isArray(expectedError)) { + assert.ok(expectedError.includes(err.code), err); + } else { + assert.strictEqual(err.code, expectedError); + } + })); + } server.listen(0, '127.0.0.1', common.mustCall(() => { const args = ['s_client', '-connect', `127.0.0.1:${server.address().port}`, '-cipher', `${ciphers}:@SECLEVEL=1`]; - execFile(opensslCli, args, common.mustSucceed((stdout) => { + execFile(opensslCli, args, common.mustCall((err, stdout, stderr) => { + if (expectedError) { + assert.strictEqual(err?.code, 1); + if (expectedError instanceof RegExp) assert.match(stderr, expectedError); + server.close(); + return; + } + + assert.ifError(err); assert(keylen === null || // s_client < OpenSSL 3.5 stdout.includes(`Server Temp Key: DH, ${keylen} bits`) || @@ -103,10 +121,10 @@ function test(dhparam, keylen, expectedCipher) { return once(server, 'close'); } -function testCustomParam(keylen, expectedCipher) { +function testCustomParam(keylen, expectedCipher, expectedError) { const dhparam = loadDHParam(keylen); if (keylen === 'error') keylen = null; - return test(dhparam, keylen, expectedCipher); + return test(dhparam, keylen, expectedCipher, expectedError); } (async () => { @@ -140,14 +158,29 @@ function testCustomParam(keylen, expectedCipher) { // OpenSSL 4.0 implements RFC 7919 FFDHE negotiation for TLS 1.2 and // ignores the server-supplied dhparam in favor of FFDHE-2048, so the // negotiated key length is always 2048. - if (secLevel < 2) { - await testCustomParam(1024, dheCipher); - } else if (hasOpenSSL(4, 0)) { - await test(loadDHParam(3072), 2048, dheCipher); + if (hasFIPS(3)) { + if (hasFIPS(4)) { + await test(loadDHParam(3072), 2048, dheCipher); + await testCustomParam(2048, dheCipher); + } else { + const errorCode = hasFIPS(3, 5) ? + [ + 'ERR_SSL_INVALID_KEY_LENGTH', + 'ERR_SSL_SSL/TLS_ALERT_ILLEGAL_PARAMETER', + ] : 'ERR_SSL_INTERNAL_ERROR'; + await testCustomParam(3072, null, errorCode); + await testCustomParam(2048, null, errorCode); + } } else { - await testCustomParam(3072, dheCipher); + if (secLevel < 2) { + await testCustomParam(1024, dheCipher); + } else if (hasOpenSSL(4, 0)) { + await test(loadDHParam(3072), 2048, dheCipher); + } else { + await testCustomParam(3072, dheCipher); + } + await testCustomParam(2048, dheCipher); } - await testCustomParam(2048, dheCipher); // Invalid DHE parameters are discarded. Prior to OpenSSL 4.0 this // disabled DHE and ECDHE was negotiated; since 4.0, FFDHE-2048 is used. diff --git a/test/parallel/test-tls-ecdh-multiple.js b/test/parallel/test-tls-ecdh-multiple.js index ed60044197d7..b01051d5b457 100644 --- a/test/parallel/test-tls-ecdh-multiple.js +++ b/test/parallel/test-tls-ecdh-multiple.js @@ -8,7 +8,11 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } -const { opensslCli, hasOpenSSL } = require('../common/crypto'); +const { + opensslCli, + hasOpenSSL, + hasFIPS, +} = require('../common/crypto'); const crypto = require('crypto'); if (!opensslCli) { @@ -19,14 +23,16 @@ const assert = require('assert'); const tls = require('tls'); const { execFile } = require('child_process'); const fixtures = require('../common/fixtures'); +const fips3 = hasFIPS(3); function loadPEM(n) { return fixtures.readKey(`${n}.pem`); } -// OpenSSL 4.0 disables support for deprecated elliptic curves from RFC 8422 -// (including secp256k1) by default. -const ecdhCurve = process.features.openssl_is_boringssl || hasOpenSSL(4, 0) ? +// The FIPS provider and OpenSSL 4.0 disable support for deprecated elliptic +// curves from RFC 8422 (including secp256k1) by default. +const ecdhCurve = process.features.openssl_is_boringssl || + hasOpenSSL(4, 0) || hasFIPS(3) ? 'prime256v1:secp521r1' : 'secp256k1:prime256v1:secp521r1'; @@ -61,8 +67,17 @@ const server = tls.createServer(options, (conn) => { 'prime192v3', ]; - // Brainpool is not supported in FIPS mode. - if (crypto.getFips()) { + // Setting a Brainpool group on a TLS context is deferred by OpenSSL, so + // exercise the prohibited key operation directly under FIPS properties. + if (fips3) { + if (hasFIPS(3, 5)) { + assert.throws( + () => crypto.createECDH('brainpoolP256r1').generateKeys(), + { code: 'ERR_CRYPTO_OPERATION_FAILED' }); + } else { + unsupportedCurves.push('brainpoolP256r1'); + } + } else if (crypto.getFips() === 1) { unsupportedCurves.push('brainpoolP256r1'); } diff --git a/test/parallel/test-tls-env-extra-ca-with-options.js b/test/parallel/test-tls-env-extra-ca-with-options.js index 8f04decf670c..5775284eea44 100644 --- a/test/parallel/test-tls-env-extra-ca-with-options.js +++ b/test/parallel/test-tls-env-extra-ca-with-options.js @@ -8,7 +8,9 @@ if (!common.hasCrypto) const assert = require('node:assert'); const tls = require('node:tls'); const { fork } = require('node:child_process'); +const { hasFIPS } = require('../common/crypto'); const fixtures = require('../common/fixtures'); +const fips3 = hasFIPS(3); const tests = [ { @@ -28,13 +30,26 @@ const tests = [ crl: fixtures.readKey('ca2-crl.pem') } }, - { +]; + +if (fips3) { + assert.throws(() => tls.createSecureContext({ + pfx: fixtures.readKey('agent1.pfx'), + passphrase: 'sample', + }), { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + }); +} + +if (!fips3 || hasFIPS(3, 5)) { + tests.push({ clientOptions: { - pfx: fixtures.readKey('agent1.pfx'), - passphrase: 'sample' + pfx: fixtures.readKey(fips3 ? + 'agent1-fips.pfx' : 'agent1.pfx'), + passphrase: fips3 ? 'password' : 'sample' } - }, -]; + }); +} if (process.argv[2]) { const testNumber = parseInt(process.argv[2], 10); diff --git a/test/parallel/test-tls-getprotocol.js b/test/parallel/test-tls-getprotocol.js index 2945ff99b5a2..2fe971444271 100644 --- a/test/parallel/test-tls-getprotocol.js +++ b/test/parallel/test-tls-getprotocol.js @@ -3,7 +3,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); // This test ensures that `getProtocol` returns the right protocol // from a TLS connection @@ -11,6 +11,7 @@ const { hasOpenSSL } = require('../common/crypto'); const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); +const fips3 = hasFIPS(3); let clientConfigs = [ { @@ -42,27 +43,50 @@ const serverConfig = { }; if (!process.features.openssl_is_boringssl) { - serverConfig.ciphers = 'RSA@SECLEVEL=0'; + serverConfig.ciphers = fips3 ? + 'ECDHE-RSA-AES256-GCM-SHA384' : 'RSA@SECLEVEL=0'; } -const server = tls.createServer(serverConfig, common.mustCall(clientConfigs.length)) +const expectedConnections = fips3 ? 1 : clientConfigs.length; +const server = tls.createServer(serverConfig, common.mustCall(expectedConnections)); + +if (fips3) { + server.on('tlsClientError', common.mustCall((err) => { + assert.ok([ + 'ERR_SSL_NO_SUITABLE_DIGEST_ALGORITHM', + 'ERR_SSL_UNEXPECTED_MESSAGE', + ].includes(err.code), err); + }, 2)); +} + +server .listen(0, common.localhostIPv4, common.mustCall(function() { - let connected = 0; + let completed = 0; + function done() { + if (++completed === clientConfigs.length) + server.close(); + } + for (const v of clientConfigs) { - tls.connect({ + const shouldConnect = !fips3 || v.version === 'TLSv1.2'; + const client = tls.connect({ host: common.localhostIPv4, port: server.address().port, ciphers: v.ciphers, rejectUnauthorized: false, secureProtocol: v.secureProtocol - }, common.mustCall(function() { + }, shouldConnect ? common.mustCall(function() { assert.strictEqual(this.getProtocol(), v.version); this.on('end', common.mustCall()); this.on('close', common.mustCall(function() { assert.strictEqual(this.getProtocol(), null); + done(); })).end(); - if (++connected === clientConfigs.length) - server.close(); - })); + }) : common.mustNotCall()); + + if (!shouldConnect) { + client.on('error', common.mustCall((err) => assert(err.code))); + client.on('close', common.mustCall(done)); + } } })); diff --git a/test/parallel/test-tls-honorcipherorder.js b/test/parallel/test-tls-honorcipherorder.js index d86a59aa4cdc..fbee483d83b9 100644 --- a/test/parallel/test-tls-honorcipherorder.js +++ b/test/parallel/test-tls-honorcipherorder.js @@ -8,6 +8,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); const mustCall = common.mustCall; const tls = require('tls'); const util = require('util'); @@ -16,7 +17,7 @@ const util = require('util'); // default method is updated in the future const SSL_Method = 'TLSv1_2_method'; const localhost = '127.0.0.1'; -const config = process.features.openssl_is_boringssl ? { +const config = process.features.openssl_is_boringssl || hasFIPS(3) ? { serverCiphers: 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256', clientPreferenceCiphers: diff --git a/test/parallel/test-tls-invalid-pfx.js b/test/parallel/test-tls-invalid-pfx.js index c16858f0f788..749d9e8b9f76 100644 --- a/test/parallel/test-tls-invalid-pfx.js +++ b/test/parallel/test-tls-invalid-pfx.js @@ -7,6 +7,7 @@ const fixtures = require('../common/fixtures'); const { assert, connect, keys } = require(fixtures.path('tls-connect')); +const { hasFIPS } = require('../common/crypto'); const invalidPfx = fixtures.readKey('cert-without-key.pfx'); @@ -18,6 +19,10 @@ connect({ }, server: keys.agent1 }, common.mustCall((e, pair, cleanup) => { - assert.strictEqual(e.message, 'Unable to load private key from PFX data'); + if (hasFIPS(3)) { + assert.strictEqual(e.code, 'ERR_CRYPTO_UNSUPPORTED_OPERATION'); + } else { + assert.strictEqual(e.message, 'Unable to load private key from PFX data'); + } cleanup(); })); diff --git a/test/parallel/test-tls-min-max-version.js b/test/parallel/test-tls-min-max-version.js index 88dce9f4b8e6..83797238cf4b 100644 --- a/test/parallel/test-tls-min-max-version.js +++ b/test/parallel/test-tls-min-max-version.js @@ -12,7 +12,7 @@ if (process.features.openssl_is_boringssl) { const { hasOpenSSL, - hasOpenSSL3, + hasFIPS, } = require('../common/crypto'); const fixtures = require('../common/fixtures'); const { inspect } = require('util'); @@ -29,8 +29,24 @@ const DEFAULT_MAX_VERSION = tls.DEFAULT_MAX_VERSION; function test(cmin, cmax, cprot, smin, smax, sprot, proto, cerr, serr) { assert(proto || cerr || serr, 'test missing any expectations'); + const legacyProtocols = new Set([ + 'TLSv1', + 'TLSv1.1', + 'TLSv1_method', + 'TLSv1_1_method', + ]); + const expectedLegacyProtocol = proto === 'TLSv1' || proto === 'TLSv1.1'; + const legacyOnlyConfiguration = [cprot, sprot, cmax, smax] + .some((value) => legacyProtocols.has(value)); + const fipsLegacyFailure = hasFIPS(3) && + (expectedLegacyProtocol || (!proto && legacyOnlyConfiguration)); + + if (hasFIPS(3) && expectedLegacyProtocol) { + proto = undefined; + } + let ciphers; - if (hasOpenSSL3 && (proto === 'TLSv1' || proto === 'TLSv1.1' || + if (hasOpenSSL(3) && (proto === 'TLSv1' || proto === 'TLSv1.1' || proto === 'TLSv1_1_method' || proto === 'TLSv1_method' || sprot === 'TLSv1_1_method' || sprot === 'TLSv1_method')) { if (serr !== 'ERR_SSL_UNSUPPORTED_PROTOCOL') @@ -65,6 +81,27 @@ function test(cmin, cmax, cprot, smin, smax, sprot, proto, cerr, serr) { console.log('test:', u(cmin), u(cmax), u(cprot), u(smin), u(smax), u(sprot), u(ciphers), 'expect', u(proto), u(cerr), u(serr)); console.log(' ', where); + if (fipsLegacyFailure) { + const errors = [pair.client.err, pair.server.err].filter(Boolean); + assert(errors.length > 0); + const expectedCodes = new Set([ + 'ERR_SSL_NO_PROTOCOLS_AVAILABLE', + 'ERR_SSL_NO_SUITABLE_DIGEST_ALGORITHM', + 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE', + 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE', + 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION', + 'ERR_SSL_UNEXPECTED_MESSAGE', + 'ERR_SSL_UNSUPPORTED_PROTOCOL', + 'ERR_SSL_VERSION_TOO_LOW', + 'ERR_SSL_WRONG_VERSION_NUMBER', + ]); + if (hasFIPS(4)) + expectedCodes.add('ERR_SSL_TLS_ALERT_HANDSHAKE_FAILURE'); + for (const error of errors) + assert(expectedCodes.has(error.code), error); + return cleanup(); + } + if (!proto) { console.log('client', pair.client.err ? pair.client.err.code : undefined); console.log('server', pair.server.err ? pair.server.err.code : undefined); @@ -139,9 +176,9 @@ test(U, U, 'TLS_method', U, U, 'TLSv1_method', 'TLSv1'); // OpenSSL 1.1.1 and 3.0 use a different error code and alert (sent to the // client) when no protocols are enabled on the server. -const NO_PROTOCOLS_AVAILABLE_SERVER = hasOpenSSL3 ? +const NO_PROTOCOLS_AVAILABLE_SERVER = hasOpenSSL(3) ? 'ERR_SSL_NO_PROTOCOLS_AVAILABLE' : 'ERR_SSL_INTERNAL_ERROR'; -const NO_PROTOCOLS_AVAILABLE_SERVER_ALERT = hasOpenSSL3 ? +const NO_PROTOCOLS_AVAILABLE_SERVER_ALERT = hasOpenSSL(3) ? 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION' : 'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR'; // SSLv23 also means "any supported protocol" greater than the default diff --git a/test/parallel/test-tls-multi-key.js b/test/parallel/test-tls-multi-key.js index 0a9c6f108bf6..239cb8aec725 100644 --- a/test/parallel/test-tls-multi-key.js +++ b/test/parallel/test-tls-multi-key.js @@ -35,6 +35,7 @@ if (process.features.openssl_is_boringssl) { const fixtures = require('../common/fixtures'); const assert = require('assert'); const tls = require('tls'); +const { hasFIPS } = require('../common/crypto'); // Key is ordered as ec, rsa, cert is ordered as rsa, ec. test({ @@ -143,6 +144,17 @@ test({ }); function test(options) { + if (hasFIPS(3) && options.pfx) { + const serverOptions = { ...options }; + delete serverOptions.rsaCN; + delete serverOptions.eccCN; + delete serverOptions.client; + assert.throws(() => tls.createServer(serverOptions), { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + }); + return; + } + const rsaCN = options.rsaCN || 'agent1'; const eccCN = options.eccCN || 'agent2'; const clientTrustRoots = options.client.ca; diff --git a/test/parallel/test-tls-multi-pfx.js b/test/parallel/test-tls-multi-pfx.js index fec697cd3b70..b5bbb6decc12 100644 --- a/test/parallel/test-tls-multi-pfx.js +++ b/test/parallel/test-tls-multi-pfx.js @@ -10,9 +10,12 @@ if (process.features.openssl_is_boringssl) { const assert = require('assert'); const tls = require('tls'); +const { hasFIPS } = require('../common/crypto'); const fixtures = require('../common/fixtures'); +const fips3 = hasFIPS(3); +const fips4 = hasFIPS(4); -const options = { +const legacyOptions = { pfx: [ { buf: fixtures.readKey('agent1.pfx'), @@ -22,6 +25,40 @@ const options = { ] }; +if (fips3) { + assert.throws(() => tls.createServer(legacyOptions), { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + }); + + if (!hasFIPS(3, 5)) { + return; + } +} + +const fipsPfx = [ + { + buf: fixtures.readKey('agent1-fips.pfx'), + passphrase: 'password', + }, + { + buf: fixtures.readKey('ec-fips.pfx'), + passphrase: 'password', + }, +]; + +if (fips4) { + for (const { buf } of fipsPfx) { + assert.throws(() => tls.createServer({ + pfx: buf, + passphrase: 'sample', + }), { + message: 'password strength too weak', + }); + } +} + +const options = fips3 ? { pfx: fipsPfx } : legacyOptions; + const ciphers = []; const server = tls.createServer(options, function(conn) { diff --git a/test/parallel/test-tls-passphrase.js b/test/parallel/test-tls-passphrase.js index 4372da249bb5..1fe2c1ec11cf 100644 --- a/test/parallel/test-tls-passphrase.js +++ b/test/parallel/test-tls-passphrase.js @@ -26,6 +26,7 @@ if (!common.hasCrypto) const assert = require('assert'); const tls = require('tls'); +const { hasFIPS } = require('../common/crypto'); const fixtures = require('../common/fixtures'); const passKey = fixtures.readKey('rsa_private_encrypted.pem'); @@ -37,6 +38,33 @@ assert(Buffer.isBuffer(cert)); assert.strictEqual(typeof passKey.toString(), 'string'); assert.strictEqual(typeof cert.toString(), 'string'); +if (hasFIPS(3)) { + const encryptedKeyOptions = { + key: passKey, + passphrase: 'password', + cert, + }; + assert.throws(() => tls.Server(encryptedKeyOptions), { + code: 'ERR_OSSL_EVP_UNSUPPORTED', + }); + assert.throws(() => tls.connect(encryptedKeyOptions), { + code: 'ERR_OSSL_EVP_UNSUPPORTED', + }); + + const server = tls.Server({ key: rawKey, passphrase: 'ignored', cert }); + server.listen(0, common.mustCall(function() { + const client = tls.connect({ + port: this.address().port, + key: rawKey, + passphrase: 'ignored', + cert, + rejectUnauthorized: false, + }, common.mustCall(() => client.end())); + client.on('close', common.mustCall(() => server.close())); + })); + return; +} + function onSecureConnect() { return common.mustCall(function() { this.end(); }); } diff --git a/test/parallel/test-tls-pfx-authorizationerror.js b/test/parallel/test-tls-pfx-authorizationerror.js index 53fcc0b16b5b..e115eea80fea 100644 --- a/test/parallel/test-tls-pfx-authorizationerror.js +++ b/test/parallel/test-tls-pfx-authorizationerror.js @@ -10,14 +10,38 @@ const fixtures = require('../common/fixtures'); const assert = require('assert'); const tls = require('tls'); +const { hasFIPS } = require('../common/crypto'); -const pfx = fixtures.readKey('agent1.pfx'); +const fips3 = hasFIPS(3); +const fips35 = hasFIPS(3, 5); +const fips4 = hasFIPS(4); +const pfx = fixtures.readKey(fips35 ? 'agent1-fips.pfx' : 'agent1.pfx'); +const passphrase = fips35 ? 'password' : 'sample'; + +if (fips3) { + assert.throws(() => tls.createServer({ + pfx: fixtures.readKey('agent1.pfx'), + passphrase: 'sample', + }), { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + }); + + if (!fips35) { + return; + } + + if (fips4) { + assert.throws(() => tls.createServer({ pfx, passphrase: 'sample' }), { + message: 'password strength too weak', + }); + } +} const server = tls .createServer( { pfx: pfx, - passphrase: 'sample', + passphrase, requestCert: true, rejectUnauthorized: false }, @@ -33,7 +57,7 @@ const server = tls { port: this.address().port, pfx: pfx, - passphrase: 'sample', + passphrase, rejectUnauthorized: false }, common.mustCall(() => { diff --git a/test/parallel/test-tls-session-cache.js b/test/parallel/test-tls-session-cache.js index ae560e567980..0789879e1dce 100644 --- a/test/parallel/test-tls-session-cache.js +++ b/test/parallel/test-tls-session-cache.js @@ -26,6 +26,7 @@ if (!common.hasCrypto) { } const { hasOpenSSL, + hasFIPS, opensslCli, } = require('../common/crypto'); @@ -59,7 +60,8 @@ function doTest(testOptions, callback) { secureProtocol: 'TLS_method', // BoringSSL supports the RSA cipher selector, but not OpenSSL's // cipher-string policy command syntax. - ciphers: isBoringSSL ? 'RSA' : 'RSA@SECLEVEL=0' + ciphers: hasFIPS(3) ? 'ECDHE-RSA-AES256-GCM-SHA384' : + (isBoringSSL ? 'RSA' : 'RSA@SECLEVEL=0') }; let requestCount = 0; let resumeCount = 0; @@ -108,8 +110,9 @@ function doTest(testOptions, callback) { server.listen(0, common.mustCall(function() { const args = [ 's_client', - isBoringSSL ? '-tls1_2' : '-tls1', - '-cipher', (hasOpenSSL(3, 1) ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), + isBoringSSL || hasFIPS(3) ? '-tls1_2' : '-tls1', + '-cipher', hasFIPS(3) ? 'ECDHE-RSA-AES256-GCM-SHA384' : + (hasOpenSSL(3, 1) ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), '-connect', `localhost:${this.address().port}`, '-servername', 'ohgod', '-key', fixtures.path('keys/rsa_private.pem'), diff --git a/test/parallel/test-tls-set-ciphers.js b/test/parallel/test-tls-set-ciphers.js index 82a19bb9e90f..57fb35991121 100644 --- a/test/parallel/test-tls-set-ciphers.js +++ b/test/parallel/test-tls-set-ciphers.js @@ -6,10 +6,10 @@ if (!common.hasCrypto) { const { hasOpenSSL, - hasOpenSSL3, + hasFIPS, } = require('../common/crypto'); -if (!hasOpenSSL3) { +if (!hasOpenSSL(3)) { common.skip('missing crypto, or OpenSSL version lower than 3'); } @@ -96,57 +96,83 @@ if (hasOpenSSL(4, 0)) { expectedTLSAlertError = 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE'; } -// Have shared ciphers. -test(U, 'AES256-SHA', 'AES256-SHA'); -test('AES256-SHA', U, 'AES256-SHA'); - -test(U, 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); -test('TLS_AES_256_GCM_SHA384', U, 'TLS_AES_256_GCM_SHA384'); -test('TLS_AES_256_GCM_SHA384:!TLS_CHACHA20_POLY1305_SHA256', U, 'TLS_AES_256_GCM_SHA384'); - -// Do not have shared ciphers. -test('TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256', - U, expectedTLSAlertError, 'ERR_SSL_NO_SHARED_CIPHER'); - -test('AES256-SHA', 'AES256-SHA256', U, expectedTLSAlertError, - 'ERR_SSL_NO_SHARED_CIPHER'); -test('AES256-SHA:TLS_AES_256_GCM_SHA384', - 'TLS_CHACHA20_POLY1305_SHA256:AES256-SHA256', - U, expectedTLSAlertError, 'ERR_SSL_NO_SHARED_CIPHER'); - -// Cipher order ignored, TLS1.3 chosen before TLS1.2. -test('AES256-SHA:TLS_AES_256_GCM_SHA384', U, 'TLS_AES_256_GCM_SHA384'); -test(U, 'AES256-SHA:TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); - -// Cipher order ignored, TLS1.3 before TLS1.2 and -// cipher suites are not disabled if TLS ciphers are set only -// TODO: maybe these tests should be reworked so maxVersion clamping -// is done explicitly and not implicitly in the test() function -test('AES256-SHA', U, 'TLS_AES_256_GCM_SHA384', U, U, { maxVersion: 'TLSv1.3' }); -test(U, 'AES256-SHA', 'TLS_AES_256_GCM_SHA384', U, U, { maxVersion: 'TLSv1.3' }); - -// TLS_AES_128_CCM_8_SHA256 & TLS_AES_128_CCM_SHA256 are not enabled by -// default, but work. -// However, for OpenSSL32 AES_128 is not enabled due to the -// default security level -if (!hasOpenSSL(3, 2)) { - test('TLS_AES_128_CCM_8_SHA256', U, - U, 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE', 'ERR_SSL_NO_SHARED_CIPHER'); - - test('TLS_AES_128_CCM_8_SHA256', 'TLS_AES_128_CCM_8_SHA256', - 'TLS_AES_128_CCM_8_SHA256'); +if (hasFIPS(3)) { + const tls12Cipher = 'ECDHE-RSA-AES256-GCM-SHA384'; + + // FIPS-approved TLS 1.2 and TLS 1.3 cipher suites work. + test(U, tls12Cipher, tls12Cipher); + test(tls12Cipher, U, tls12Cipher); + test(U, 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); + test('TLS_AES_256_GCM_SHA384', U, 'TLS_AES_256_GCM_SHA384'); + + // The FIPS provider rejects ChaCha20-Poly1305. + test('TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256', + U, expectedTLSAlertError, 'ERR_SSL_NO_CIPHERS_AVAILABLE'); + + // Invalid cipher values are still validated before provider selection. + test(9, tls12Cipher, U, 'ERR_INVALID_ARG_TYPE', U); + test(tls12Cipher, 9, U, U, 'ERR_INVALID_ARG_TYPE'); + test(':', tls12Cipher, U, 'ERR_INVALID_ARG_VALUE', U); + test(tls12Cipher, ':', U, U, 'ERR_INVALID_ARG_VALUE'); + + // Empty and null values continue to select the defaults. + test('TLS_AES_256_GCM_SHA384', '', 'TLS_AES_256_GCM_SHA384'); + test('', 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); + test(null, 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); + test('TLS_AES_256_GCM_SHA384', null, 'TLS_AES_256_GCM_SHA384'); +} else { + // Have shared ciphers. + test(U, 'AES256-SHA', 'AES256-SHA'); + test('AES256-SHA', U, 'AES256-SHA'); + + test(U, 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); + test('TLS_AES_256_GCM_SHA384', U, 'TLS_AES_256_GCM_SHA384'); + test('TLS_AES_256_GCM_SHA384:!TLS_CHACHA20_POLY1305_SHA256', U, 'TLS_AES_256_GCM_SHA384'); + + // Do not have shared ciphers. + test('TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256', + U, expectedTLSAlertError, 'ERR_SSL_NO_SHARED_CIPHER'); + + test('AES256-SHA', 'AES256-SHA256', U, expectedTLSAlertError, + 'ERR_SSL_NO_SHARED_CIPHER'); + test('AES256-SHA:TLS_AES_256_GCM_SHA384', + 'TLS_CHACHA20_POLY1305_SHA256:AES256-SHA256', + U, expectedTLSAlertError, 'ERR_SSL_NO_SHARED_CIPHER'); + + // Cipher order ignored, TLS1.3 chosen before TLS1.2. + test('AES256-SHA:TLS_AES_256_GCM_SHA384', U, 'TLS_AES_256_GCM_SHA384'); + test(U, 'AES256-SHA:TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); + + // Cipher order ignored, TLS1.3 before TLS1.2 and + // cipher suites are not disabled if TLS ciphers are set only + // TODO: maybe these tests should be reworked so maxVersion clamping + // is done explicitly and not implicitly in the test() function + test('AES256-SHA', U, 'TLS_AES_256_GCM_SHA384', U, U, { maxVersion: 'TLSv1.3' }); + test(U, 'AES256-SHA', 'TLS_AES_256_GCM_SHA384', U, U, { maxVersion: 'TLSv1.3' }); + + // TLS_AES_128_CCM_8_SHA256 & TLS_AES_128_CCM_SHA256 are not enabled by + // default, but work. + // However, for OpenSSL32 AES_128 is not enabled due to the + // default security level + if (!hasOpenSSL(3, 2)) { + test('TLS_AES_128_CCM_8_SHA256', U, + U, 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE', 'ERR_SSL_NO_SHARED_CIPHER'); + + test('TLS_AES_128_CCM_8_SHA256', 'TLS_AES_128_CCM_8_SHA256', + 'TLS_AES_128_CCM_8_SHA256'); + } + + // Invalid cipher values + test(9, 'AES256-SHA', U, 'ERR_INVALID_ARG_TYPE', U); + test('AES256-SHA', 9, U, U, 'ERR_INVALID_ARG_TYPE'); + test(':', 'AES256-SHA', U, 'ERR_INVALID_ARG_VALUE', U); + test('AES256-SHA', ':', U, U, 'ERR_INVALID_ARG_VALUE'); + + // Using '' is synonymous for "use default ciphers" + test('TLS_AES_256_GCM_SHA384', '', 'TLS_AES_256_GCM_SHA384'); + test('', 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); + + // Using null should be treated the same as undefined. + test(null, 'AES256-SHA', 'AES256-SHA'); + test('AES256-SHA', null, 'AES256-SHA'); } - -// Invalid cipher values -test(9, 'AES256-SHA', U, 'ERR_INVALID_ARG_TYPE', U); -test('AES256-SHA', 9, U, U, 'ERR_INVALID_ARG_TYPE'); -test(':', 'AES256-SHA', U, 'ERR_INVALID_ARG_VALUE', U); -test('AES256-SHA', ':', U, U, 'ERR_INVALID_ARG_VALUE'); - -// Using '' is synonymous for "use default ciphers" -test('TLS_AES_256_GCM_SHA384', '', 'TLS_AES_256_GCM_SHA384'); -test('', 'TLS_AES_256_GCM_SHA384', 'TLS_AES_256_GCM_SHA384'); - -// Using null should be treated the same as undefined. -test(null, 'AES256-SHA', 'AES256-SHA'); -test('AES256-SHA', null, 'AES256-SHA'); diff --git a/test/parallel/test-tls-write-error.js b/test/parallel/test-tls-write-error.js index 8a8d820a09cc..f6ec7b9bd245 100644 --- a/test/parallel/test-tls-write-error.js +++ b/test/parallel/test-tls-write-error.js @@ -5,7 +5,9 @@ if (!common.hasCrypto) const { TestTLSSocket, ccs } = require('../common/tls'); const fixtures = require('../common/fixtures'); +const assert = require('assert'); const https = require('https'); +const { hasFIPS } = require('../common/crypto'); // Regression test for an use-after-free bug in the TLS implementation that // would occur when `SSL_write()` failed. @@ -18,6 +20,7 @@ const opts = { key: server_key, cert: server_cert, }; +const rejectsClientHello = hasFIPS(3) && !hasFIPS(3, 5); if (!process.features.openssl_is_boringssl) { opts.ciphers = 'ALL@SECLEVEL=0'; @@ -25,7 +28,15 @@ if (!process.features.openssl_is_boringssl) { const server = https.createServer(opts, (req, res) => { res.write('hello'); -}).listen(0, common.mustCall(() => { +}); + +if (rejectsClientHello) { + server.once('tlsClientError', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_WRONG_SIGNATURE_TYPE'); + })); +} + +server.listen(0, common.mustCall(() => { const client = new TestTLSSocket(server_cert); client.connect({ @@ -37,6 +48,12 @@ const server = https.createServer(opts, (req, res) => { })); client.once('data', common.mustCall((buf) => { + if (rejectsClientHello) { + client.end(); + server.close(); + return; + } + let remaining = buf; do { remaining = client.parseTLSFrame(remaining); diff --git a/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js b/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js index 316d706e7b79..8ac5125b276c 100644 --- a/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js +++ b/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js @@ -6,10 +6,17 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const fips3 = hasFIPS(3); -async function test(algorithmName, keyLength, ivLength, format = 'raw') { +async function test( + algorithmName, + keyLength, + ivLength, + format = 'raw', + causeMessage, +) { const key = await subtle.importKey( format, new Uint8Array(keyLength), @@ -21,19 +28,40 @@ async function test(algorithmName, keyLength, ivLength, format = 'raw') { const data = new Uint8Array(32); data.buffer.transfer(); + const expected = causeMessage === undefined ? + { name: 'OperationError' } : + (err) => err.name === 'OperationError' && + err.cause?.message === causeMessage; await assert.rejects( subtle.decrypt({ name: algorithmName, iv: new Uint8Array(ivLength) }, key, data), - { name: 'OperationError' }, + expected, ); } const tests = [ test('AES-GCM', 32, 12), - test('ChaCha20-Poly1305', 32, 12, 'raw-secret'), ]; +if (fips3) { + tests.push(assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array(32), + 'ChaCha20-Poly1305', + false, + ['encrypt', 'decrypt']), + { name: 'NotSupportedError' })); +} else { + tests.push(test('ChaCha20-Poly1305', 32, 12, 'raw-secret')); +} + if (hasOpenSSL(3)) { - tests.push(test('AES-OCB', 32, 12, 'raw-secret')); + tests.push(test( + 'AES-OCB', + 32, + 12, + 'raw-secret', + fips3 ? 'error:0308010C:digital envelope routines::unsupported' : undefined)); } Promise.all(tests).then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-constructors.js b/test/parallel/test-webcrypto-constructors.js index 782265edc294..3d13b6c92bba 100644 --- a/test/parallel/test-webcrypto-constructors.js +++ b/test/parallel/test-webcrypto-constructors.js @@ -6,7 +6,9 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const fips4 = hasFIPS(4); // Test CryptoKey constructor { @@ -152,19 +154,41 @@ const notSubtle = Reflect.construct(function() {}, [], SubtleCrypto); } { - subtle.importKey( - 'raw', - globalThis.crypto.getRandomValues(new Uint8Array(4)), - 'PBKDF2', - false, - ['deriveKey'], - ).then((key) => { + const keyData = globalThis.crypto.getRandomValues( + new Uint8Array(fips4 ? 8 : 4)); + const importedKeys = [ + subtle.importKey('raw', keyData, 'PBKDF2', false, ['deriveKey']), + ]; + if (fips4) { + importedKeys.push( + subtle.importKey( + 'raw', + globalThis.crypto.getRandomValues(new Uint8Array(4)), + 'PBKDF2', + false, + ['deriveKey'])); + } + + Promise.all(importedKeys).then(async ([key, weakKey]) => { subtle.importKey = common.mustNotCall(); - return subtle.deriveKey({ + if (fips4) { + await assert.rejects(subtle.deriveKey({ + name: 'PBKDF2', + hash: 'SHA-512', + salt: new Uint8Array(), + iterations: 5, + }, weakKey, { + name: 'AES-GCM', + length: 256, + }, true, ['encrypt', 'decrypt']), { name: 'OperationError' }); + } + + await subtle.deriveKey({ name: 'PBKDF2', hash: 'SHA-512', - salt: globalThis.crypto.getRandomValues(new Uint8Array()), - iterations: 5, + salt: globalThis.crypto.getRandomValues( + new Uint8Array(fips4 ? 16 : 0)), + iterations: fips4 ? 1000 : 5, }, key, { name: 'AES-GCM', length: 256 diff --git a/test/parallel/test-webcrypto-cryptokey-hidden-slots.js b/test/parallel/test-webcrypto-cryptokey-hidden-slots.js index 792a1a59c4c5..75b977073dab 100644 --- a/test/parallel/test-webcrypto-cryptokey-hidden-slots.js +++ b/test/parallel/test-webcrypto-cryptokey-hidden-slots.js @@ -22,6 +22,7 @@ if (!common.hasCrypto) const assert = require('node:assert'); const { createHmac, + getFips, KeyObject, sign: cryptoSign, verify: cryptoVerify, @@ -50,7 +51,7 @@ common.expectWarning({ const { publicKey: rsaPublicKey } = await subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, diff --git a/test/parallel/test-webcrypto-deduplicate-usages.js b/test/parallel/test-webcrypto-deduplicate-usages.js index ebd1b4683fc6..09601de2348c 100644 --- a/test/parallel/test-webcrypto-deduplicate-usages.js +++ b/test/parallel/test-webcrypto-deduplicate-usages.js @@ -13,7 +13,7 @@ if (!common.hasCrypto) const assert = require('assert'); const { createSecretKey } = require('crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; function assertSameSet(actual, expected, msg) { @@ -50,6 +50,10 @@ function assertSameSet(actual, expected, msg) { expected: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'] }, ]; + if (hasFIPS(3)) + symmetric.splice(symmetric.findIndex(({ algorithm }) => + algorithm.name === 'ChaCha20-Poly1305'), 1); + if (hasOpenSSL(3)) { symmetric.push({ algorithm: { name: 'AES-OCB', length: 128 }, @@ -107,6 +111,10 @@ function assertSameSet(actual, expected, msg) { privateExpected: ['deriveKey', 'deriveBits'] }, ]; + if (hasFIPS(3)) + asymmetric.splice(asymmetric.findIndex(({ algorithm }) => + algorithm.name === 'X25519'), 1); + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { asymmetric.push({ algorithm: { name: 'ML-DSA-65' }, @@ -322,12 +330,17 @@ function assertSameSet(actual, expected, msg) { // ChaCha20-Poly1305 raw-secret import. tests.push((async () => { - const key = await subtle.importKey( + const imported = subtle.importKey( 'raw-secret', new Uint8Array(32), { name: 'ChaCha20-Poly1305' }, true, ['decrypt', 'encrypt', 'decrypt', 'encrypt']); + if (hasFIPS(3)) { + await assert.rejects(imported, { name: 'NotSupportedError' }); + return; + } + const key = await imported; assertSameSet(key.usages, ['encrypt', 'decrypt']); assert.strictEqual(key.usages.length, 2); })()); @@ -477,6 +490,10 @@ function assertSameSet(actual, expected, msg) { privateExpected: ['deriveKey', 'deriveBits'] }, ]; + if (hasFIPS(3)) + jwkPairVectors.splice(jwkPairVectors.findIndex(({ algorithm }) => + algorithm.name === 'X25519'), 1); + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { jwkPairVectors.push({ algorithm: { name: 'ML-DSA-65' }, diff --git a/test/parallel/test-webcrypto-derivebits-argon2.js b/test/parallel/test-webcrypto-derivebits-argon2.js index e2b465ab206b..129b716c5919 100644 --- a/test/parallel/test-webcrypto-derivebits-argon2.js +++ b/test/parallel/test-webcrypto-derivebits-argon2.js @@ -5,10 +5,12 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3, 2)) common.skip('requires OpenSSL >= 3.2'); +if (hasFIPS(3)) + common.skip('Argon2 is not available in FIPS mode'); const assert = require('assert'); const { createSecretKey } = require('crypto'); diff --git a/test/parallel/test-webcrypto-derivebits-cfrg.js b/test/parallel/test-webcrypto-derivebits-cfrg.js index 757c81272536..35671cb0f2cf 100644 --- a/test/parallel/test-webcrypto-derivebits-cfrg.js +++ b/test/parallel/test-webcrypto-derivebits-cfrg.js @@ -6,7 +6,9 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const rejectsXCurves = hasFIPS(3, 5); const kTests = [ { @@ -77,6 +79,15 @@ async function prepareKeys() { Object.keys(keys).map(async (name) => { const { size, result, privateKey, publicKey } = keys[name]; + if (rejectsXCurves) { + await assert.rejects( + subtle.deriveBits({ name, public: publicKey }, privateKey, 8 * size), + (err) => err.name === 'OperationError' && + err.cause?.message === + 'error:03000096:digital envelope routines::operation not supported for this keytype'); + return; + } + { // Good parameters const bits = await subtle.deriveBits({ diff --git a/test/parallel/test-webcrypto-derivebits.js b/test/parallel/test-webcrypto-derivebits.js index 545dcab58495..41a02175afc5 100644 --- a/test/parallel/test-webcrypto-derivebits.js +++ b/test/parallel/test-webcrypto-derivebits.js @@ -7,7 +7,11 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const requiresLongHkdfInputs = hasFIPS(3) && !hasFIPS(3, 5); +const rejectsWeakPbkdf2Inputs = hasFIPS(4); +const rejectsXCurves = hasFIPS(3, 5); // This is only a partial test. The WebCrypto Web Platform Tests // will provide much greater coverage. @@ -56,12 +60,20 @@ const { subtle } = globalThis.crypto; } const kTests = [ - ['hello', 'there', 'my friend', 'SHA-256', 512, - '14d93b0ccd99d4f2cbd9fbfe9c830b5b8a43e3e45e329' + - '41ef21bdeb0fa87b6b6bfa5c54466aa5bf76cdc2685fb' + - 'a4408ea5b94c049fe035649b46f92fdc519374'], - ['hello', 'there', 'my friend', 'SHA-384', 128, - 'e36cf2cf943d8f3a88adb80f478745c3'], + [requiresLongHkdfInputs ? 'hello hello hello' : 'hello', + 'there', requiresLongHkdfInputs ? 'my friend indeed' : 'my friend', + 'SHA-256', 512, + requiresLongHkdfInputs ? + 'bc2b7841512a6f4563f723c317909ac305ddbfbdec1daf0055d0587b5db8d635' + + 'a22f97b0dfbcc12dcd2d096123385227b16e95e5bccc0d6751491f38c5e48428' : + '14d93b0ccd99d4f2cbd9fbfe9c830b5b8a43e3e45e329' + + '41ef21bdeb0fa87b6b6bfa5c54466aa5bf76cdc2685fb' + + 'a4408ea5b94c049fe035649b46f92fdc519374'], + [requiresLongHkdfInputs ? 'hello hello hello' : 'hello', + 'there', requiresLongHkdfInputs ? 'my friend indeed' : 'my friend', + 'SHA-384', 128, + requiresLongHkdfInputs ? 'ee2d1d7dc759c26f2ab8ee6d7cfa0c23' : + 'e36cf2cf943d8f3a88adb80f478745c3'], ]; const tests = Promise.all(kTests.map((args) => test(...args))); @@ -88,17 +100,24 @@ const { subtle } = globalThis.crypto; } const kTests = [ - ['hello', 'there', 10, 'SHA-256', 512, - 'f72d1cf4853fffbd16a42751765d11f8dc7939498ee7b7' + - 'ce7678b4cb16fad88098110a83e71f4483ce73203f7a64' + - '719d293280f780f9fafdcf46925c5c0588b3'], - ['hello', 'there', 5, 'SHA-384', 128, - '201509b012c9cd2fbe7ea938f0c509b3'], + ['password', 'there there here', 1000, 'SHA-256', 512, + '8802c34ee684a523f9304a6335394c0a5f02350d51383d' + + '17d3cf89fa0808591ddede3c832fe4691c7f361ade53b9' + + '36bf94347055bcf86fd662abe038fb945d17'], + ['password', 'there there here', 2000, 'SHA-384', 128, + '7c650b88798cea1a390802a6f97e05b0'], ]; const tests = Promise.all(kTests.map((args) => test(...args))); tests.then(common.mustCall()); + + if (rejectsWeakPbkdf2Inputs) { + assert.rejects( + test('hello', 'there', 10, 'SHA-256', 512), + { name: 'OperationError' }) + .then(common.mustCall()); + } } // Test PBKDF2 rejects iteration counts beyond the native signed int range @@ -145,10 +164,20 @@ const { subtle } = globalThis.crypto; assert.deepStrictEqual(secret1, secret2); } - test('X25519').then(common.mustCall()); - if (!process.features.openssl_is_boringssl) { - test('X448').then(common.mustCall()); + if (rejectsXCurves) { + for (const name of ['X25519', 'X448']) { + assert.rejects( + test(name), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported') + .then(common.mustCall()); + } } else { - common.printSkipMessage('Skipping unsupported X448 test case'); + test('X25519').then(common.mustCall()); + if (!process.features.openssl_is_boringssl) { + test('X448').then(common.mustCall()); + } else { + common.printSkipMessage('Skipping unsupported X448 test case'); + } } } diff --git a/test/parallel/test-webcrypto-derivekey-cfrg.js b/test/parallel/test-webcrypto-derivekey-cfrg.js index c5a5b1f3518f..a8cfa1d26671 100644 --- a/test/parallel/test-webcrypto-derivekey-cfrg.js +++ b/test/parallel/test-webcrypto-derivekey-cfrg.js @@ -6,7 +6,9 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const rejectsXCurves = hasFIPS(3, 5); const kTests = [ { @@ -80,6 +82,15 @@ async function prepareKeys() { Object.keys(keys).map(async (name) => { const { result, privateKey, publicKey } = keys[name]; + if (rejectsXCurves) { + await assert.rejects( + subtle.deriveKey({ name, public: publicKey }, privateKey, ...otherArgs), + (err) => err.name === 'OperationError' && + err.cause?.message === + 'error:03000096:digital envelope routines::operation not supported for this keytype'); + return; + } + { // Good parameters const key = await subtle.deriveKey({ diff --git a/test/parallel/test-webcrypto-derivekey.js b/test/parallel/test-webcrypto-derivekey.js index f9323bca2caf..e3b4293983cc 100644 --- a/test/parallel/test-webcrypto-derivekey.js +++ b/test/parallel/test-webcrypto-derivekey.js @@ -5,11 +5,13 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); const assert = require('assert'); const { subtle } = globalThis.crypto; const { KeyObject } = require('crypto'); +const rejectsXCurves = hasFIPS(3, 5); +const fips4 = hasFIPS(4); // This is only a partial test. The WebCrypto Web Platform Tests // will provide much greater coverage. @@ -74,24 +76,24 @@ const { KeyObject } = require('crypto'); } const kTests = [ - ['hello', 'there', 'my friend', 'SHA-1', - '365ca5d3f42d050c74302e420c83975327950f1913a151eecd00526bf52614a0'], - ['hello', 'there', 'my friend', 'SHA-256', - '14d93b0ccd99d4f2cbd9fbfe9c830b5b8a43e3e45e32941ef21bdeb0fa87b6b6'], - ['hello', 'there', 'my friend', 'SHA-384', - 'e36cf2cf943d8f3a88adb80f478745c336ac811b1a86d03a7d10eb0b6b52295c'], - ['hello', 'there', 'my friend', 'SHA-512', - '1e42d43fcacba361716f65853bd5f3c479f679612f0180eab3c51ed6c9d2b47d'], + ['hello hello hello', 'there', 'my friend indeed', 'SHA-1', + 'aac1ecdc73147af6a418393da6875bff5f566c0a473e25d54b4dfc3cb7cb2ace'], + ['hello hello hello', 'there', 'my friend indeed', 'SHA-256', + 'bc2b7841512a6f4563f723c317909ac305ddbfbdec1daf0055d0587b5db8d635'], + ['hello hello hello', 'there', 'my friend indeed', 'SHA-384', + 'ee2d1d7dc759c26f2ab8ee6d7cfa0c2313e82650a4514673c867063dc1849040'], + ['hello hello hello', 'there', 'my friend indeed', 'SHA-512', + 'a7abd704d0be364c6d4a530b6f93fcaff95474a2eee5a127ff86c5d095a2a812'], ]; if (!process.features.openssl_is_boringssl) { kTests.push( - ['hello', 'there', 'my friend', 'SHA3-256', - '2a49a3b6fb219117af9e251c6c65f16600cbca13bd0be6e70d96b0b9fa4cf3fd'], - ['hello', 'there', 'my friend', 'SHA3-384', - '0437bb59b95f2db2c7684c0b439028cb0fdd6f0f5d03b9f489066a87ae147221'], - ['hello', 'there', 'my friend', 'SHA3-512', - '3bbc469d38214371921e52c6f147e96cb7eb370421a81f53dea8b4851dfb8bce'], + ['hello hello hello', 'there', 'my friend indeed', 'SHA3-256', + '89b3751df2ada85322a57ec82f7d0a5c233c6def91c92e681bc5118bd5768dca'], + ['hello hello hello', 'there', 'my friend indeed', 'SHA3-384', + 'b4fa7b9929a595bbaa370eb959b194c1232d5a329abd02a5fa166a1424962fcf'], + ['hello hello hello', 'there', 'my friend indeed', 'SHA3-512', + 'ac5d90a6bc848961e78a491887539b29c532a9c0d0b39cec464df071a63e0061'], ); } else { common.printSkipMessage('Skipping unsupported SHA-3 test cases'); @@ -127,30 +129,36 @@ const { KeyObject } = require('crypto'); } const kTests = [ - ['hello', 'there', 5, 'SHA-1', - 'f8f65a5fd92c9b74916083a7e9b0001c46bc89e2a14c48014cf1e0e1dbabf635'], - ['hello', 'there', 5, 'SHA-256', - '2e575eae24267db32106c7dba01615e5417557e8c5cf33ba15a311cb0c2907ee'], - ['hello', 'there', 5, 'SHA-384', - '201509b012c9cd2fbe7ea938f0c509b36ecb140f38bf9130e96923f55f46756d'], - ['hello', 'there', 5, 'SHA-512', - '2e8d981741f98193e0af9c79870af0e985089341221edad9a130d297eae1984b'], + ['hello hello hello', 'my friend indeed', 1000, 'SHA-1', + 'b747604ca226287ccae90d8d8c119645a80d1154625a56b2debb3f9b172eb134'], + ['hello hello hello', 'my friend indeed', 1000, 'SHA-256', + '3cc64f6cfcbdb9c42b63b471016f17d1966b70934b4719a12ce95382940252f2'], + ['hello hello hello', 'my friend indeed', 1000, 'SHA-384', + '5ce64241beef3a3931dbfac6eef7303b5bdbea13449d4eeb4f89c3e9f9357c65'], + ['hello hello hello', 'my friend indeed', 1000, 'SHA-512', + '12790ce09027db067d680670f4dc704715b5120d139e8fde810afc34fb66f9f1'], ]; if (!process.features.openssl_is_boringssl) { kTests.push( - ['hello', 'there', 5, 'SHA3-256', - '0aed29b61b3ca3978aea34a9793276574ea997b69e8d03727438199f90571649'], - ['hello', 'there', 5, 'SHA3-384', - '7aa4a274aa19b4623c5d3091c4b06355de85ff6f25e53a83e3126cbb86ae68df'], - ['hello', 'there', 5, 'SHA3-512', - '4d909c47a81c625f866d1f9406248e6bc3c7ea89225fbccf1f08820254c9ef56'] + ['hello hello hello', 'my friend indeed', 1000, 'SHA3-256', + '0f69b46660cba27b95215d5676492c64ed6abf6d426669a4a02b0ca3a1c36c11'], + ['hello hello hello', 'my friend indeed', 1000, 'SHA3-384', + 'a2e86a2d4cdf9844d70ae37f71302356ce2b9a899f5d778fc9af64d32e351d70'], + ['hello hello hello', 'my friend indeed', 1000, 'SHA3-512', + '03431052c37d626ae3fc1df582ff2a4d610642fc27e1b8130ca5980c0b0756ac'] ); } else { common.printSkipMessage('Skipping unsupported SHA-3 test cases'); } - const tests = Promise.all(kTests.map((args) => test(...args))); + const promises = kTests.map((args) => test(...args)); + if (fips4) { + promises.push(assert.rejects( + test('hello', 'there', 5, 'SHA-256', ''), + { name: 'OperationError' })); + } + const tests = Promise.all(promises); tests.then(common.mustCall()); } @@ -254,8 +262,18 @@ const { KeyObject } = require('crypto'); (async () => { for (const [derivedKeyAlgorithm, usage, expected] of vectors) { const derived = await subtle.deriveKey( - { name: 'PBKDF2', salt: new Uint8Array([]), hash: 'SHA-256', iterations: 20 }, - await subtle.importKey('raw', new Uint8Array([]), { name: 'PBKDF2' }, false, ['deriveKey']), + { + name: 'PBKDF2', + salt: new Uint8Array(16), + hash: 'SHA-256', + iterations: 1000, + }, + await subtle.importKey( + 'raw', + new Uint8Array(8), + { name: 'PBKDF2' }, + false, + ['deriveKey']), derivedKeyAlgorithm, false, [usage]); @@ -271,17 +289,27 @@ if (hasOpenSSL(3)) { const usages = ['sign']; for (const [algorithm, baseKeyAlgorithm] of [ [ - { name: 'HKDF', salt: new Uint8Array(), info: new Uint8Array(), hash: 'SHA-256' }, + { + name: 'HKDF', + salt: new Uint8Array(16), + info: new Uint8Array(), + hash: 'SHA-256', + }, { name: 'HKDF' }, ], [ - { name: 'PBKDF2', salt: new Uint8Array(), hash: 'SHA-256', iterations: 20 }, + { + name: 'PBKDF2', + salt: new Uint8Array(16), + hash: 'SHA-256', + iterations: 1000, + }, { name: 'PBKDF2' }, ], ]) { const baseKey = await subtle.importKey( 'raw', - new Uint8Array(), + new Uint8Array(baseKeyAlgorithm.name === 'HKDF' ? 16 : 8), baseKeyAlgorithm, false, ['deriveKey']); @@ -293,11 +321,15 @@ if (hasOpenSSL(3)) { usages); assert.strictEqual(derived.algorithm.length, 0); - const signature = await subtle.sign({ + const signature = subtle.sign({ name: 'KMAC128', outputLength: 256, }, derived, new Uint8Array()); - assert.strictEqual(signature.byteLength, 32); + if (fips4) { + await assert.rejects(signature, { name: 'OperationError' }); + } else { + assert.strictEqual((await signature).byteLength, 32); + } } })().then(common.mustCall()); } @@ -333,10 +365,20 @@ if (hasOpenSSL(3)) { assert.deepStrictEqual(raw1, raw2); } - test('X25519').then(common.mustCall()); - if (!process.features.openssl_is_boringssl) { - test('X448').then(common.mustCall()); + if (rejectsXCurves) { + for (const name of ['X25519', 'X448']) { + assert.rejects( + test(name), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported') + .then(common.mustCall()); + } } else { - common.printSkipMessage('Skipping unsupported X448 test case'); + test('X25519').then(common.mustCall()); + if (!process.features.openssl_is_boringssl) { + test('X448').then(common.mustCall()); + } else { + common.printSkipMessage('Skipping unsupported X448 test case'); + } } } diff --git a/test/parallel/test-webcrypto-digest.js b/test/parallel/test-webcrypto-digest.js index 8e1b6797ee86..447948212bc1 100644 --- a/test/parallel/test-webcrypto-digest.js +++ b/test/parallel/test-webcrypto-digest.js @@ -9,7 +9,8 @@ const assert = require('assert'); const { Buffer } = require('buffer'); const { subtle } = globalThis.crypto; const { createHash, getHashes } = require('crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); +const fips4 = hasFIPS(4); const kTests = [ ['SHA-1', ['sha1'], 160], @@ -397,18 +398,35 @@ if (getHashes().includes('shake128')) { 'ca6f88db415829', }, ]) { - assert.strictEqual( - Buffer.from(await subtle.digest(algorithm, data)).toString('hex'), - expected); + const digest = subtle.digest(algorithm, data); + if (fips4) { + await assert.rejects( + digest, + (err) => err.name === 'OperationError' && + err.cause?.code === 'ERR_OSSL_EVP_UNSUPPORTED'); + } else { + assert.strictEqual( + Buffer.from(await digest).toString('hex'), + expected); + } } - const truncated = Buffer.from(await subtle.digest( + const truncatedDigest = subtle.digest( { ...nistCShakeSample1.algorithm, outputLength: 255 }, - nistCShakeSample1.data)); - const expected = Buffer.from(nistCShakeSample1.expected, 'hex'); - assert.strictEqual(truncated.byteLength, expected.byteLength); - assert.deepStrictEqual(truncated.subarray(0, 31), expected.subarray(0, 31)); - assert.strictEqual(truncated[31] & 0b00000001, 0); - assert.strictEqual(truncated[31] | 0b00000001, expected[31]); + nistCShakeSample1.data); + if (fips4) { + await assert.rejects( + truncatedDigest, + (err) => err.name === 'OperationError' && + err.cause?.code === 'ERR_OSSL_EVP_UNSUPPORTED'); + } else { + const truncated = Buffer.from(await truncatedDigest); + const expected = Buffer.from(nistCShakeSample1.expected, 'hex'); + assert.strictEqual(truncated.byteLength, expected.byteLength); + assert.deepStrictEqual( + truncated.subarray(0, 31), expected.subarray(0, 31)); + assert.strictEqual(truncated[31] & 0b00000001, 0); + assert.strictEqual(truncated[31] | 0b00000001, expected[31]); + } })().then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-aes.js b/test/parallel/test-webcrypto-encrypt-decrypt-aes.js index d7a7dca6584c..14cc0127188a 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt-aes.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt-aes.js @@ -8,6 +8,7 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); const assert = require('assert'); +const { getFips } = require('crypto'); const { subtle } = globalThis.crypto; async function testEncrypt({ keyBuffer, algorithm, plaintext, result }) { @@ -237,6 +238,14 @@ if (hasOpenSSL(3)) { } = require('../fixtures/crypto/aes_ocb')(); (async function() { + if (getFips() === 1) { + await assert.rejects( + testEncrypt(passing[0]), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + return; + } + const variations = []; passing.forEach((vector) => { diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js b/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js index 723fd26ea570..45225115fd52 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js @@ -6,8 +6,19 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +if (hasFIPS(3)) { + assert.rejects( + subtle.generateKey( + { name: 'ChaCha20-Poly1305' }, + false, + ['encrypt', 'decrypt']), + { name: 'NotSupportedError' }).then(common.mustCall()); + return; +} + async function testEncrypt({ keyBuffer, algorithm, plaintext, result }) { // Using a copy of plaintext to prevent tampering of the original plaintext = Buffer.from(plaintext); diff --git a/test/parallel/test-webcrypto-encrypt-decrypt.js b/test/parallel/test-webcrypto-encrypt-decrypt.js index c4ca52862fe0..76c8c66d7db4 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt.js @@ -7,6 +7,7 @@ if (!common.hasCrypto) const assert = require('assert'); const { hasOpenSSL } = require('../common/crypto'); +const { getFips } = require('crypto'); const { subtle } = globalThis.crypto; // This is only a partial test. The WebCrypto Web Platform Tests @@ -207,7 +208,15 @@ if (hasOpenSSL(3)) { Buffer.from(buf).toString('hex')); } - test().then(common.mustCall()); + if (getFips() === 1) { + assert.rejects( + test(), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported') + .then(common.mustCall()); + } else { + test().then(common.mustCall()); + } } else { common.printSkipMessage('Skipping unsupported AES-OCB test cases'); } diff --git a/test/parallel/test-webcrypto-export-import-cfrg.js b/test/parallel/test-webcrypto-export-import-cfrg.js index cd71906f7c79..21c187848023 100644 --- a/test/parallel/test-webcrypto-export-import-cfrg.js +++ b/test/parallel/test-webcrypto-export-import-cfrg.js @@ -8,7 +8,9 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const rejectsXCurves = hasFIPS(3, 5); const keyData = { 'Ed25519': { @@ -404,9 +406,19 @@ async function testImportRaw({ name, publicUsages }) { for (const extractable of [true, false]) { tests.push(testImportSpki(vector, extractable)); tests.push(testImportPkcs8(vector, extractable)); - tests.push(testImportJwk(vector, extractable)); + if (rejectsXCurves && vector.name.startsWith('X')) { + tests.push(assert.rejects( + testImportJwk(vector, extractable), + { name: 'DataError' })); + } else { + tests.push(testImportJwk(vector, extractable)); + } + } + if (rejectsXCurves && vector.name.startsWith('X')) { + tests.push(assert.rejects(testImportRaw(vector), { name: 'DataError' })); + } else { + tests.push(testImportRaw(vector)); } - tests.push(testImportRaw(vector)); } await Promise.all(tests); })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-export-import.js b/test/parallel/test-webcrypto-export-import.js index c7399c69d9c9..9f6b2125d8ed 100644 --- a/test/parallel/test-webcrypto-export-import.js +++ b/test/parallel/test-webcrypto-export-import.js @@ -10,7 +10,12 @@ const { hasOpenSSL } = require('../common/crypto'); const assert = require('assert'); const { subtle } = globalThis.crypto; -const { createPrivateKey, createPublicKey, createSecretKey } = require('crypto'); +const { + createPrivateKey, + createPublicKey, + createSecretKey, + getFips, +} = require('crypto'); { async function test() { @@ -397,7 +402,7 @@ if (hasOpenSSL(3)) { async function test() { const { publicKey, privateKey } = await subtle.generateKey({ name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-384' }, true, ['sign', 'verify']); diff --git a/test/parallel/test-webcrypto-get-public-key.mjs b/test/parallel/test-webcrypto-get-public-key.mjs index 622ec4adca68..6ca3e8d45125 100644 --- a/test/parallel/test-webcrypto-get-public-key.mjs +++ b/test/parallel/test-webcrypto-get-public-key.mjs @@ -9,8 +9,10 @@ import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const { kSupportedAlgorithms } = require('internal/crypto/util'); +const { hasFIPS } = require('../common/crypto'); const { SubtleCrypto } = globalThis; const { subtle } = globalThis.crypto; +const rejectsXCurves = hasFIPS(3, 5); const RSA_KEY_GEN = { modulusLength: 2048, @@ -80,6 +82,15 @@ for (const name of Object.keys(kSupportedAlgorithms.exportKey)) { assert.strictEqual(SubtleCrypto.supports('getPublicKey', name), true); + if (rejectsXCurves && + (name === 'X25519' || name === 'X448')) { + await assert.rejects( + subtle.generateKey(test.algorithm, false, test.privateUsages), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + continue; + } + const { privateKey } = await subtle.generateKey( test.algorithm, false, test.privateUsages); const usages = test.publicUsages; diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index 86d480740bb2..ca56e394b14d 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -6,7 +6,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); const assert = require('assert'); const { types: { isCryptoKey } } = require('util'); @@ -17,6 +17,8 @@ const { } = require('crypto'); const { subtle } = globalThis.crypto; const rsaMinimumModulusLength = getFips() === 1 ? 2048 : 512; +const fips3 = hasFIPS(3); +const fips35 = hasFIPS(3, 5); const { bigIntArrayToUnsignedBigInt } = require('internal/crypto/util'); @@ -71,7 +73,7 @@ const vectors = { }, 'RSASSA-PKCS1-v1_5': { algorithm: { - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, @@ -83,7 +85,7 @@ const vectors = { }, 'RSA-PSS': { algorithm: { - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, @@ -95,7 +97,7 @@ const vectors = { }, 'RSA-OAEP': { algorithm: { - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, @@ -249,6 +251,21 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { // Test bad usages { async function test(name) { + if (fips3 && name === 'ChaCha20-Poly1305') { + await assert.rejects( + subtle.generateKey({ name }, true, []), + { name: 'NotSupportedError' }); + return; + } + + if (fips35 && (name === 'X25519' || name === 'X448')) { + await assert.rejects( + subtle.generateKey({ name }, true, ['deriveBits']), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + return; + } + await assert.rejects( subtle.generateKey( { @@ -469,7 +486,7 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { const kTests = [ [ 'RSASSA-PKCS1-v1_5', - 1024, + getFips() === 1 ? 2048 : 1024, Buffer.from([1, 0, 1]), 'SHA-1', ['sign'], @@ -477,7 +494,7 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { ], [ 'RSA-PSS', - 1024, + getFips() === 1 ? 2048 : 1024, Buffer.from([1, 0, 1]), 'SHA-256', ['sign'], @@ -486,22 +503,37 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { ]; + let fipsExponentTest; if (!process.features.openssl_is_boringssl) { - kTests.push( - [ - 'RSA-OAEP', - 1024, - Buffer.from([3]), - 'SHA3-256', - ['decrypt', 'unwrapKey'], - ['encrypt', 'wrapKey'], - ], - ); + if (fips3) { + fipsExponentTest = assert.rejects( + subtle.generateKey({ + name: 'RSA-OAEP', + modulusLength: 2048, + publicExponent: Buffer.from([3]), + hash: 'SHA3-256', + }, true, ['decrypt', 'unwrapKey', 'encrypt', 'wrapKey']), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:020000B2:rsa routines::pub exponent out of range'); + } else { + kTests.push( + [ + 'RSA-OAEP', + 1024, + Buffer.from([3]), + 'SHA3-256', + ['decrypt', 'unwrapKey'], + ['encrypt', 'wrapKey'], + ], + ); + } } else { common.printSkipMessage('Skipping unsupported SHA-3 test case'); } const tests = kTests.map((args) => test(...args)); + if (fipsExponentTest !== undefined) + tests.push(fipsExponentTest); Promise.all(tests).then(common.mustCall()); } @@ -722,6 +754,13 @@ assert.throws(() => new CryptoKey(), { code: 'ERR_ILLEGAL_CONSTRUCTOR' }); // Test OKP Key Generation { + async function testFipsUnsupported(name) { + await assert.rejects( + subtle.generateKey({ name }, true, ['deriveKey', 'deriveBits']), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + } + async function test( name, privateUsages, @@ -786,7 +825,12 @@ assert.throws(() => new CryptoKey(), { code: 'ERR_ILLEGAL_CONSTRUCTOR' }); common.printSkipMessage('Skipping unsupported Curve448 test cases'); } - const tests = kTests.map((args) => test(...args)); + const tests = kTests.map((args) => { + const [name] = args; + if (fips35 && (name === 'X25519' || name === 'X448')) + return testFipsUnsupported(name); + return test(...args); + }); Promise.all(tests).then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs index da5df46390bb..3fc63f4db29c 100644 --- a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs @@ -24,7 +24,21 @@ if (!common.hasCrypto) common.skip('missing crypto'); const require = createRequire(import.meta.url); const { kSupportedAlgorithms } = require('internal/crypto/util'); +const { getFips } = require('node:crypto'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const fips3 = hasFIPS(3); +const fips35 = hasFIPS(3, 5); +const fips4 = hasFIPS(4); +const fips35UnavailableKeyGeneration = new Set([ + 'X25519', + 'X448', +]); +const fips3UnavailableDerivation = new Set([ + 'Argon2d', + 'Argon2i', + 'Argon2id', +]); Promise.prototype.then = common.mustNotCall('Promise.prototype.then'); @@ -333,7 +347,7 @@ function algorithm(name, params = {}) { function rsaAlgorithm(name) { return algorithm(name, { - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }); @@ -877,8 +891,8 @@ for (const name of ['HKDF', 'PBKDF2']) { }) : algorithm(name, { hash: 'SHA-256', - salt: new Uint8Array(8), - iterations: 1, + salt: new Uint8Array(fips4 ? 16 : 8), + iterations: fips4 ? 1000 : 1, }), })); } @@ -1040,6 +1054,24 @@ for (const [name, operations] of supportedAlgorithms) { assert(fixture, `missing prototype pollution fixture for ${name}`); const ctx = { __proto__: null }; + if (fips3 && fips3UnavailableDerivation.has(name)) { + await fixture.importKey(ctx); + await assert.rejects( + fixture.deriveBits(ctx), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + continue; + } + if ((fips3 && name === 'ChaCha20-Poly1305') || + (fips35 && fips35UnavailableKeyGeneration.has(name))) { + const expected = name === 'ChaCha20-Poly1305' ? + { name: 'NotSupportedError' } : + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'; + await assert.rejects(fixture.generateKey(ctx), expected); + continue; + } + for (const operation of operationOrder) { if (!operations.has(operation)) continue; @@ -1047,7 +1079,17 @@ for (const [name, operations] of supportedAlgorithms) { typeof fixture[operation], 'function', `missing prototype pollution coverage for ${name} ${operation}`); - await fixture[operation](ctx); + if (fips3 && name === 'AES-OCB' && + (operation === 'encrypt' || operation === 'decrypt')) { + if (operation === 'decrypt') + ctx.ciphertext = new Uint8Array(); + await assert.rejects( + fixture[operation](ctx), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + } else { + await fixture[operation](ctx); + } } if (typeof fixture.getPublicKey === 'function' && @@ -1085,17 +1127,22 @@ for (const name of getKeyLengthAlgorithms) { continue; } - await assertCryptoKeyResult(`get key length ${name}`, () => + const deriveKey = () => assertCryptoKeyResult(`get key length ${name}`, () => subtle.deriveKey( algorithm('PBKDF2', { hash: 'SHA-256', - salt: new Uint8Array(8), - iterations: 1, + salt: new Uint8Array(fips4 ? 16 : 8), + iterations: fips4 ? 1000 : 1, }), pbkdf2Key, target.algorithm, true, target.usages)); + if (fips3 && name === 'ChaCha20-Poly1305') { + await assert.rejects(deriveKey(), { name: 'NotSupportedError' }); + } else { + await deriveKey(); + } } // Keep one explicit unwrapKey('jwk') negative case: the parsed object must not diff --git a/test/parallel/test-webcrypto-raw-format-aliases.js b/test/parallel/test-webcrypto-raw-format-aliases.js index 94e9474fde13..bce0c353904d 100644 --- a/test/parallel/test-webcrypto-raw-format-aliases.js +++ b/test/parallel/test-webcrypto-raw-format-aliases.js @@ -6,7 +6,9 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const rejectsXCurves = hasFIPS(3, 5); function getAlgorithmName(algorithm) { return typeof algorithm === 'string' ? algorithm : algorithm.name; @@ -50,7 +52,7 @@ async function assertPublicKeyDoesNotAcceptRawSecret( importUsages); } -Promise.all([ +const tests = [ assertSecretKeyDoesNotAcceptRawPublic('HKDF'), assertSecretKeyDoesNotAcceptRawPublic('PBKDF2'), assertPublicKeyDoesNotAcceptRawSecret( @@ -65,8 +67,18 @@ Promise.all([ 'Ed25519', ['sign', 'verify'], ['verify']), - assertPublicKeyDoesNotAcceptRawSecret( +]; + +if (rejectsXCurves) { + tests.push(assert.rejects( + subtle.generateKey('X25519', true, ['deriveBits']), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported')); +} else { + tests.push(assertPublicKeyDoesNotAcceptRawSecret( 'X25519', ['deriveBits'], - []), -]).then(common.mustCall()); + [])); +} + +Promise.all(tests).then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-sign-verify-ecdsa.js b/test/parallel/test-webcrypto-sign-verify-ecdsa.js index eb7814efa556..94e3eff02eb9 100644 --- a/test/parallel/test-webcrypto-sign-verify-ecdsa.js +++ b/test/parallel/test-webcrypto-sign-verify-ecdsa.js @@ -6,7 +6,10 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { getFips } = require('crypto'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const rejectsSha1Signing = hasFIPS(3) && !hasFIPS(3, 5); const vectors = require('../fixtures/crypto/ecdsa')(); @@ -50,7 +53,7 @@ async function testVerify({ name, subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, @@ -173,7 +176,7 @@ async function testSign({ name, subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, @@ -229,6 +232,32 @@ async function testSign({ name, for (let i = 0; i < vectors.length; ++i) { const vector = vectors[i]; + if (rejectsSha1Signing && vector.hash === 'SHA-1') { + const publicKey = await subtle.importKey( + 'spki', + vector.publicKeyBuffer, + { name: vector.name, namedCurve: vector.namedCurve }, + false, + ['verify']); + const privateKey = await subtle.importKey( + 'pkcs8', + vector.privateKeyBuffer, + { name: vector.name, namedCurve: vector.namedCurve }, + false, + ['sign']); + assert(await subtle.verify( + { name: vector.name, hash: vector.hash }, + publicKey, + vector.signature, + vector.plaintext)); + await assert.rejects( + subtle.sign( + { name: vector.name, hash: vector.hash }, + privateKey, + vector.plaintext), + { name: 'OperationError' }); + continue; + } variations.push(testVerify(vector)); variations.push(testSign(vector)); } diff --git a/test/parallel/test-webcrypto-sign-verify-eddsa.js b/test/parallel/test-webcrypto-sign-verify-eddsa.js index b35e94df44c9..e34a3d43d2a3 100644 --- a/test/parallel/test-webcrypto-sign-verify-eddsa.js +++ b/test/parallel/test-webcrypto-sign-verify-eddsa.js @@ -92,7 +92,7 @@ async function testVerify({ name, subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: crypto.getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, @@ -217,7 +217,7 @@ async function testSign({ name, subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: crypto.getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, diff --git a/test/parallel/test-webcrypto-sign-verify-hmac.js b/test/parallel/test-webcrypto-sign-verify-hmac.js index ac3841fad79a..bbe5dd498109 100644 --- a/test/parallel/test-webcrypto-sign-verify-hmac.js +++ b/test/parallel/test-webcrypto-sign-verify-hmac.js @@ -6,6 +6,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { getFips } = require('crypto'); const { subtle } = globalThis.crypto; const vectors = require('../fixtures/crypto/hmac')(); @@ -35,7 +36,7 @@ async function testVerify({ hash, subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, @@ -126,7 +127,7 @@ async function testSign({ hash, subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, diff --git a/test/parallel/test-webcrypto-sign-verify-kmac.js b/test/parallel/test-webcrypto-sign-verify-kmac.js index f93fc293b2a4..160067b9b760 100644 --- a/test/parallel/test-webcrypto-sign-verify-kmac.js +++ b/test/parallel/test-webcrypto-sign-verify-kmac.js @@ -5,16 +5,29 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3)) common.skip('requires OpenSSL >= 3'); const assert = require('assert'); const { subtle } = globalThis.crypto; +const fips4 = hasFIPS(4); const vectors = require('../fixtures/crypto/kmac')(); +function isFipsUnsupported(err) { + return err.name === 'OperationError' && + err.cause?.code === 'ERR_OSSL_EVP_UNSUPPORTED'; +} + +function isFips4Incompatible({ key, keyLength, outputLength }) { + const keyLengthInBits = keyLength ?? key.byteLength * 8; + return keyLengthInBits < 128 || + keyLengthInBits % 8 !== 0 || + outputLength % 8 !== 0; +} + async function testVerify({ algorithm, key, keyLength, @@ -193,8 +206,13 @@ async function testSign({ algorithm, const variations = []; for (const vector of vectors) { - variations.push(testVerify(vector)); - variations.push(testSign(vector)); + if (fips4 && isFips4Incompatible(vector)) { + variations.push(assert.rejects(testVerify(vector), isFipsUnsupported)); + variations.push(assert.rejects(testSign(vector), isFipsUnsupported)); + } else { + variations.push(testVerify(vector)); + variations.push(testSign(vector)); + } } await Promise.all(variations); @@ -209,25 +227,47 @@ async function testSign({ algorithm, ['sign', 'verify']); const algorithm = { name: 'KMAC128', - outputLength: 9, + outputLength: fips4 ? 16 : 9, customization: new Uint8Array(), }; const data = new Uint8Array([1, 2, 3]); + if (fips4) { + await assert.rejects( + subtle.sign({ ...algorithm, outputLength: 9 }, key, data), + isFipsUnsupported); + } + const signature = await subtle.sign(algorithm, key, data); assert.strictEqual(signature.byteLength, 2); - assert.strictEqual(new Uint8Array(signature)[1] & 0b01111111, 0); + if (!fips4) + assert.strictEqual(new Uint8Array(signature)[1] & 0b01111111, 0); assert(await subtle.verify(algorithm, key, signature, data)); - const signature16 = new Uint8Array(await subtle.sign({ - ...algorithm, - outputLength: 16, - }, key, data)); - signature16[1] &= 0b10000000; - assert.notDeepStrictEqual(new Uint8Array(signature), signature16); + if (fips4) { + const signature128 = await subtle.sign({ + ...algorithm, + outputLength: 128, + }, key, data); + assert.strictEqual(signature128.byteLength, 16); + assert(await subtle.verify({ + ...algorithm, + outputLength: 128, + }, key, signature128, data)); + } else { + const signature16 = new Uint8Array(await subtle.sign({ + ...algorithm, + outputLength: 16, + }, key, data)); + signature16[1] &= 0b10000000; + assert.notDeepStrictEqual(new Uint8Array(signature), signature16); + } const invalidSignature = new Uint8Array(signature); - invalidSignature[1] |= 0b00000001; + if (fips4) + invalidSignature[0] ^= 0b00000001; + else + invalidSignature[1] |= 0b00000001; assert(!(await subtle.verify(algorithm, key, invalidSignature, data))); const nonByteKey = await subtle.importKey( @@ -236,15 +276,20 @@ async function testSign({ algorithm, { name: 'KMAC128', length: 25 }, false, ['sign', 'verify']); - const nonByteKeySignature = await subtle.sign({ + const nonByteKeySignature = subtle.sign({ ...algorithm, outputLength: 16, }, nonByteKey, data); - assert.strictEqual(nonByteKeySignature.byteLength, 2); - assert(await subtle.verify({ - ...algorithm, - outputLength: 16, - }, nonByteKey, nonByteKeySignature, data)); + if (fips4) { + await assert.rejects(nonByteKeySignature, isFipsUnsupported); + } else { + const result = await nonByteKeySignature; + assert.strictEqual(result.byteLength, 2); + assert(await subtle.verify({ + ...algorithm, + outputLength: 16, + }, nonByteKey, result, data)); + } })().then(common.mustCall()); (async function() { @@ -265,9 +310,14 @@ async function testSign({ algorithm, assert.strictEqual(key.algorithm.length, keyData.byteLength * 8); const algorithm = { name, outputLength: 256 }; - const signature = await subtle.sign(algorithm, key, data); - assert.strictEqual(signature.byteLength, 32); - assert(await subtle.verify(algorithm, key, signature, data)); + const signature = subtle.sign(algorithm, key, data); + if (fips4) { + await assert.rejects(signature, isFipsUnsupported); + } else { + const result = await signature; + assert.strictEqual(result.byteLength, 32); + assert(await subtle.verify(algorithm, key, result, data)); + } } } })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js index 67f90d2a0e53..ba6eab08efb1 100644 --- a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js +++ b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js @@ -43,7 +43,7 @@ async function testVerify({ name, subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: crypto.getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, @@ -154,7 +154,7 @@ async function testSign({ name, subtle.generateKey( { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: crypto.getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }, diff --git a/test/parallel/test-webcrypto-sign-verify-rsa.js b/test/parallel/test-webcrypto-sign-verify-rsa.js index 3f8a916846ce..45803a72fad8 100644 --- a/test/parallel/test-webcrypto-sign-verify-rsa.js +++ b/test/parallel/test-webcrypto-sign-verify-rsa.js @@ -6,7 +6,10 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const fips3 = hasFIPS(3); +const rejectsSha1Signing = hasFIPS(3) && !hasFIPS(3, 5); const rsa_pkcs = require('../fixtures/crypto/rsa_pkcs'); const rsa_pss = require('../fixtures/crypto/rsa_pss'); @@ -194,6 +197,23 @@ async function testSign({ }); } +async function testFipsSignRejected({ + algorithm, + hash, + privateKeyBuffer, + plaintext, +}) { + const privateKey = await subtle.importKey( + 'pkcs8', + privateKeyBuffer, + { name: algorithm.name, hash }, + false, + ['sign']); + await assert.rejects( + subtle.sign(algorithm, privateKey, plaintext), + { name: 'OperationError' }); +} + async function testSaltLength(keyLength, hash, hLen) { const { publicKey, privateKey } = await subtle.generateKey({ name: 'RSA-PSS', @@ -234,14 +254,30 @@ async function testSaltLength(keyLength, hash, hLen) { rsa_pkcs().forEach((vector) => { variations.push(testVerify(vector)); - variations.push(testSign(vector)); + variations.push(rejectsSha1Signing && vector.hash === 'SHA-1' ? + testFipsSignRejected(vector) : testSign(vector)); }); rsa_pss().forEach((vector) => { variations.push(testVerify(vector)); - variations.push(testSign(vector)); + variations.push(rejectsSha1Signing && vector.hash === 'SHA-1' ? + testFipsSignRejected(vector) : testSign(vector)); }); - for (const keyLength of [1024, 2048]) { + if (fips3) { + variations.push(assert.rejects( + subtle.generateKey({ + name: 'RSA-PSS', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, false, ['sign', 'verify']), + { + name: 'OperationError', + message: 'algorithm.modulusLength must be at least 2048', + })); + } + + for (const keyLength of fips3 ? [2048] : [1024, 2048]) { for (const [hash, hLen] of [ ['SHA-1', 20], ['SHA-256', 32], @@ -253,6 +289,8 @@ async function testSaltLength(keyLength, hash, hLen) { ['SHA3-512', 64], ] : []), ]) { + if (rejectsSha1Signing && hash === 'SHA-1') + continue; variations.push(testSaltLength(keyLength, hash, hLen)); } } diff --git a/test/parallel/test-webcrypto-sign-verify.js b/test/parallel/test-webcrypto-sign-verify.js index 0a6f5cffe7b9..db6c9e093872 100644 --- a/test/parallel/test-webcrypto-sign-verify.js +++ b/test/parallel/test-webcrypto-sign-verify.js @@ -8,6 +8,7 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); const assert = require('assert'); +const { getFips } = require('crypto'); const { subtle } = globalThis.crypto; // This is only a partial test. The WebCrypto Web Platform Tests @@ -19,7 +20,7 @@ const { subtle } = globalThis.crypto; const ec = new TextEncoder(); const { publicKey, privateKey } = await subtle.generateKey({ name: 'RSASSA-PKCS1-v1_5', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, true, ['sign', 'verify']); diff --git a/test/parallel/test-webcrypto-supports.mjs b/test/parallel/test-webcrypto-supports.mjs index d0a88f0057b8..6c3b2c640fd7 100644 --- a/test/parallel/test-webcrypto-supports.mjs +++ b/test/parallel/test-webcrypto-supports.mjs @@ -4,6 +4,11 @@ if (!common.hasCrypto) common.skip('missing crypto'); import * as assert from 'node:assert'; +import { hasFIPS } from '../common/crypto.js'; + +if (hasFIPS(3)) + common.skip('SubtleCrypto.supports() does not reflect FIPS provider availability'); + const { SubtleCrypto } = globalThis; const sources = [ diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index 2078ec617a14..9695ee03298d 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -5,10 +5,13 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); const assert = require('assert'); +const { getFips } = require('crypto'); const { subtle } = globalThis.crypto; +const fips3 = hasFIPS(3); +const fips35 = hasFIPS(3, 5); const kWrappingData = { 'RSA-OAEP': { @@ -35,7 +38,7 @@ const kWrappingData = { wrap: { iv: new Uint8Array(16), additionalData: new Uint8Array(16), - tagLength: 64 + tagLength: fips3 ? 128 : 64 }, pair: false }, @@ -54,7 +57,10 @@ const kWrappingData = { } }; -if (hasOpenSSL(3)) { +if (fips3) + delete kWrappingData['ChaCha20-Poly1305']; + +if (hasOpenSSL(3) && !fips3) { kWrappingData['AES-OCB'] = { generate: { length: 128 }, wrap: { @@ -87,7 +93,7 @@ async function generateKeysToWrap() { { algorithm: { name: 'RSASSA-PKCS1-v1_5', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, @@ -98,7 +104,7 @@ async function generateKeysToWrap() { { algorithm: { name: 'RSA-PSS', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, @@ -109,7 +115,7 @@ async function generateKeysToWrap() { { algorithm: { name: 'RSA-OAEP', - modulusLength: 1024, + modulusLength: getFips() === 1 ? 2048 : 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, @@ -234,6 +240,20 @@ async function generateKeysToWrap() { common.printSkipMessage('Skipping unsupported Curve test cases'); } + if (fips3) { + const unsupported = new Set([ + 'ChaCha20-Poly1305', + ]); + if (fips35) { + unsupported.add('X25519'); + unsupported.add('X448'); + } + for (let i = parameters.length - 1; i >= 0; --i) { + if (unsupported.has(parameters[i].algorithm.name)) + parameters.splice(i, 1); + } + } + const allkeys = await Promise.all(parameters.map(async (params) => { const usages = 'usages' in params ? params.usages : @@ -360,6 +380,37 @@ function testWrapping(name, keys) { } (async function() { + if (fips3) { + await assert.rejects( + subtle.generateKey( + { name: 'ChaCha20-Poly1305' }, true, ['wrapKey']), + { name: 'NotSupportedError' }); + + if (fips35) { + for (const name of ['X25519', 'X448']) { + await assert.rejects( + subtle.generateKey({ name }, true, ['deriveBits']), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + } + } + + const wrappingKey = await subtle.generateKey( + { name: 'AES-OCB', length: 128 }, true, ['wrapKey']); + const key = await subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256', length: 256 }, + true, + ['sign']); + await assert.rejects( + subtle.wrapKey( + 'raw', + key, + wrappingKey, + { name: 'AES-OCB', iv: new Uint8Array(15), tagLength: 128 }), + (err) => err.name === 'OperationError' && + err.cause?.message === 'error:0308010C:digital envelope routines::unsupported'); + } + await generateWrappingKeys(); const keys = await generateKeysToWrap(); const variations = []; diff --git a/test/pummel/test-crypto-argon2-nonblocking-constructor.js b/test/pummel/test-crypto-argon2-nonblocking-constructor.js index 37cb5363d43c..4e0ff50c3fa3 100644 --- a/test/pummel/test-crypto-argon2-nonblocking-constructor.js +++ b/test/pummel/test-crypto-argon2-nonblocking-constructor.js @@ -4,10 +4,12 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3, 2)) common.skip('requires OpenSSL >= 3.2'); +if (hasFIPS(3)) + common.skip('Argon2 is not available in FIPS mode'); // Regression test for https://github.com/nodejs/node/issues/62861. // `AdditionalConfig` used to invoke the full Argon2 KDF synchronously inside diff --git a/test/pummel/test-crypto-dh-keys.js b/test/pummel/test-crypto-dh-keys.js index 8aa1e30e354f..824628732a55 100644 --- a/test/pummel/test-crypto-dh-keys.js +++ b/test/pummel/test-crypto-dh-keys.js @@ -32,11 +32,21 @@ if (common.isPi()) { const assert = require('assert'); const crypto = require('crypto'); +const { hasFIPS } = require('../common/crypto'); for (const name of ['modp1', 'modp2', 'modp5', 'modp14', 'modp15', 'modp16', 'modp17']) { // modp1 is 768 bits, FIPS requires >= 1024. // BoringSSL does not support modp1 or modp2. - if ((name === 'modp1' && crypto.getFips()) || + if (hasFIPS(3) && ['modp1', 'modp2', 'modp5'].includes(name)) { + const parameters = crypto.getDiffieHellman(name); + const group = crypto.createDiffieHellman( + parameters.getPrime(), parameters.getGenerator()); + assert.throws(() => group.generateKeys(), { + code: 'ERR_CRYPTO_OPERATION_FAILED', + }); + continue; + } + if ((name === 'modp1' && crypto.getFips() === 1) || (process.features.openssl_is_boringssl && (name === 'modp1' || name === 'modp2'))) { common.printSkipMessage(`Skipping unsupported ${name} test case`); diff --git a/test/pummel/test-dh-regr.js b/test/pummel/test-dh-regr.js index 961bea3246fb..c442fbc3a809 100644 --- a/test/pummel/test-dh-regr.js +++ b/test/pummel/test-dh-regr.js @@ -32,15 +32,30 @@ if (common.isPi()) { const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL, hasFIPS } = require('../common/crypto'); -// FIPS requires length >= 1024 but we use 512/256 in this test to keep it from -// taking too long and timing out in CI. -const length = crypto.getFips() ? 1024 : hasOpenSSL3 ? 512 : 256; +let p; +let iterations = 2000; +if (hasFIPS(3)) { + assert.throws(() => crypto.createDiffieHellman(1024), { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + }); -const p = crypto.createDiffieHellman(length).getPrime(); + // Use a precomputed approved group instead of generating a 2048-bit prime + // for every test run. Its larger keys also make each pummel iteration more + // expensive, so use enough iterations to exercise the regression without + // making the FIPS job excessively slow. + p = crypto.getDiffieHellman('modp14').getPrime(); + iterations = 100; +} else { + // FIPS requires length >= 1024, but small parameters keep this pummel test + // from timing out in ordinary CI. + const length = crypto.getFips() === 1 ? 1024 : (hasOpenSSL(3) ? 512 : 256); + p = crypto.createDiffieHellman(length).getPrime(); +} -for (let i = 0; i < 2000; i++) { +for (let i = 0; i < iterations; i++) { const a = crypto.createDiffieHellman(p); const b = crypto.createDiffieHellman(p); diff --git a/test/pummel/test-webcrypto-derivebits-pbkdf2.js b/test/pummel/test-webcrypto-derivebits-pbkdf2.js index bfb01ac0c94f..9e6cdcad8ab0 100644 --- a/test/pummel/test-webcrypto-derivebits-pbkdf2.js +++ b/test/pummel/test-webcrypto-derivebits-pbkdf2.js @@ -11,7 +11,9 @@ if (common.isPi()) { } const assert = require('assert'); +const { hasFIPS } = require('../common/crypto'); const { subtle } = globalThis.crypto; +const fips4 = hasFIPS(4); function getDeriveKeyInfo(name, length, hash, ...usages) { return [{ name, length, hash }, usages]; @@ -632,6 +634,19 @@ async function testWrongKeyType( Object.keys(kDerivations[size][saltSize][hash]) .forEach((iterations) => { const args = [baseKeys, size, saltSize, hash, iterations | 0]; + if (fips4 && + (size === 'empty' || saltSize !== 'long' || iterations < 1000)) { + variations.push(assert.rejects( + testDeriveBits(...args), { name: 'OperationError' })); + kDerivedKeyTypes.forEach((keyType) => { + const keyArgs = getDeriveKeyInfo(...keyType); + variations.push(assert.rejects( + testDeriveKey(...args, ...keyArgs), + { name: 'OperationError' })); + }); + return; + } + variations.push(testDeriveBits(...args)); variations.push(testDeriveBitsBadLengths(...args)); variations.push(testDeriveBitsBadHash(...args)); @@ -674,12 +689,17 @@ async function testWrongKeyType( // https://github.com/w3c/webcrypto/pull/380 { - crypto.subtle.importKey('raw', new Uint8Array(0), 'PBKDF2', false, ['deriveBits']).then((key) => { + crypto.subtle.importKey( + 'raw', + new Uint8Array(fips4 ? 8 : 0), + 'PBKDF2', + false, + ['deriveBits']).then((key) => { return crypto.subtle.deriveBits({ name: 'PBKDF2', hash: { name: 'SHA-256' }, - iterations: 10, - salt: new Uint8Array(0), + iterations: fips4 ? 1000 : 10, + salt: new Uint8Array(fips4 ? 16 : 0), }, key, 0); }).then((bits) => { assert.deepStrictEqual(bits, new ArrayBuffer(0)); diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index 5db4a7763158..ef48f457a878 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -139,6 +139,7 @@ function testInitialized(req, ctor_name) { if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check const crypto = require('crypto'); + const { hasFIPS } = require('../common/crypto'); // The handle for PBKDF2 and RandomBytes isn't returned by the function call, // so need to check it from the callback. @@ -152,7 +153,8 @@ if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check testInitialized(this, 'RandomBytesJob'); })); - if (typeof internalBinding('crypto').ScryptJob === 'function') { + if (typeof internalBinding('crypto').ScryptJob === 'function' && + !hasFIPS(3)) { crypto.scrypt('password', 'salt', 8, common.mustCall(function() { testInitialized(this, 'ScryptJob'); })); diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index db856a75cca2..33e10d491c15 100644 --- a/test/wpt/status/WebCryptoAPI.cjs +++ b/test/wpt/status/WebCryptoAPI.cjs @@ -1,14 +1,20 @@ 'use strict'; -const { hasOpenSSL } = require('../../common/crypto.js'); +const { + hasOpenSSL, + hasFIPS, +} = require('../../common/crypto.js'); const conditionalFileSkips = {}; const conditionalSubtestSkips = {}; function skip(...files) { for (const file of files) { + const provider = process.features.openssl_is_boringssl ? + 'BoringSSL' : + `OpenSSL ${process.versions.openssl}${hasFIPS(3) ? ' FIPS mode' : ''}`; conditionalFileSkips[file] = { - 'skip': 'Unsupported in ' + (process.features.openssl_is_boringssl ? 'BoringSSL' : `OpenSSL ${process.versions.openssl}`), + 'skip': `Unsupported in ${provider}`, }; } } @@ -37,7 +43,7 @@ if (!hasOpenSSL(3, 0)) { 'sign_verify/kmac.tentative.https.any.js'); } -if (!hasOpenSSL(3, 2)) { +if (!hasOpenSSL(3, 2) || hasFIPS(3)) { skip( 'derive_bits_keys/argon2.tentative.https.any.js', 'import_export/Argon2_importKey.tentative.https.any.js'); @@ -94,6 +100,80 @@ if (process.features.openssl_is_boringssl) { ['supports-modern.tentative.https.any.js', /ml-kem-512/i]); } +if (hasFIPS(3)) { + skip( + 'encrypt_decrypt/aes_ocb.tentative.https.any.js', + 'encrypt_decrypt/chacha20_poly1305.tentative.https.any.js', + 'generateKey/failures_chacha20_poly1305.tentative.https.any.js', + 'generateKey/successes_chacha20_poly1305.tentative.https.any.js', + 'import_export/ChaCha20-Poly1305_importKey.tentative.https.any.js', + 'serialization/chacha20-poly1305.tentative.https.any.js'); + + skipSubtests( + [ + 'supports-modern.tentative.https.any.js', + /(?:ChaCha20-Poly1305|^supports returns (?:true|false) for algorithm objects with (?:valid|invalid) parameters$)/, + ], + [ + 'wrapKey_unwrapKey/wrapKey_unwrapKey.https.any.js', + /(?=.*(?:RSASSA-PKCS1-v1_5|RSA-PSS|RSA-OAEP) private key)(?=.*non-extractable)/, + ]); +} + +// OpenSSL 3.0 through 3.3 reject SHA-1 signature generation in FIPS mode. +// OpenSSL 3.4 permits it for legacy use cases while marking the operation as +// non-approved through a per-operation FIPS indicator. Node does not expose +// that indicator, so the round-trip tests succeed. +if (hasFIPS(3) && !hasOpenSSL(3, 4)) { + skipSubtests( + ['sign_verify/ecdsa.https.any.js', /with SHA-1.*round trip$/], + ['sign_verify/rsa_pkcs.https.any.js', /with SHA-1.*round trip$/], + ['sign_verify/rsa_pss.https.any.js', /with SHA-1.*round trip$/]); +} + +if (hasFIPS(3, 5)) { + skip( + 'derive_bits_keys/cfrg_curves_bits_curve25519.https.any.js', + 'derive_bits_keys/cfrg_curves_bits_curve448.tentative.https.any.js', + 'derive_bits_keys/cfrg_curves_keys_curve25519.https.any.js', + 'derive_bits_keys/cfrg_curves_keys_curve448.tentative.https.any.js', + 'generateKey/successes_X25519.https.any.js', + 'generateKey/successes_X448.tentative.https.any.js', + 'import_export/okp_importKey_X25519.https.any.js', + 'import_export/okp_importKey_X448.tentative.https.any.js', + 'import_export/okp_importKey_failures_X25519.https.any.js', + 'import_export/okp_importKey_failures_X448.tentative.https.any.js', + 'serialization/x25519.https.any.js', + 'serialization/x448.tentative.https.any.js'); + + skipSubtests( + [ + 'derive_bits_keys/derived_bits_length.https.any.js', + /^X25519 derivation/, + ], + ['getPublicKey.tentative.https.any.js', /(?:X25519|X448)/], + [ + 'import_export/raw_format_aliases.tentative.https.any.js', + /(?:X25519|X448)/, + ], + [ + 'supports.tentative.https.any.js', + /(?:X25519|^deriveKey promise tests$|^supports validates the ECDH public key$)/, + ], + [ + 'wrapKey_unwrapKey/wrapKey_unwrapKey.https.any.js', + /(?=.*(?:X25519|X448))(?=.*(?:jwk|as non-extractable using pkcs8))/, + ]); +} + +if (hasFIPS(4)) { + skipSubtests( + [ + 'derive_bits_keys/pbkdf2.https.any.js', + /(?:empty password|(?:short|empty) salt|with 1 iterations)/, + ]); +} + skipSubtests( ['digest/kangarootwelve.tentative.https.any.js', /C=(?:\d{4,}|5(?:1[3-9]|[2-9]\d)|[6-9]\d{2}) bytes/]); From 6bbcbbcca041872af964898680caa28cf750e64a Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 11 Aug 2026 21:34:34 +0200 Subject: [PATCH 02/22] crypto: disable non-FIPS WebCrypto paths in FIPS mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hide TurboSHAKE and KangarooTwelve when FIPS is enabled. Reject cSHAKE and KMAC parameters that require implementations outside the OpenSSL provider, while keeping provider-backed paths available. Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/65172 Reviewed-By: Matteo Collina Reviewed-By: Yagiz Nizipli Reviewed-By: Tobias Nießen --- lib/internal/crypto/mac.js | 4 + lib/internal/crypto/util.js | 14 ++ lib/internal/crypto/webidl.js | 51 +++-- src/crypto/crypto_hash.cc | 5 + src/crypto/crypto_kmac.cc | 2 + src/crypto/crypto_turboshake.cc | 10 + src/crypto/crypto_util.cc | 8 +- src/crypto/crypto_util.h | 1 + .../test-crypto-key-objects-to-crypto-key.js | 28 ++- test/parallel/test-webcrypto-derivekey.js | 8 +- .../test-webcrypto-digest-turboshake-rfc.js | 5 + .../test-webcrypto-digest-turboshake.js | 5 + test/parallel/test-webcrypto-digest.js | 42 ++-- test/parallel/test-webcrypto-export-import.js | 106 +++++----- .../test-webcrypto-fips-exceptions.mjs | 199 ++++++++++++++++++ test/parallel/test-webcrypto-keygen-kmac.js | 49 +++-- .../test-webcrypto-prototype-pollution.mjs | 36 ++-- .../test-webcrypto-sign-verify-kmac.js | 69 +++--- test/parallel/test-webcrypto-wrap-unwrap.js | 2 +- test/wpt/status/WebCryptoAPI.cjs | 12 +- 20 files changed, 473 insertions(+), 183 deletions(-) create mode 100644 test/parallel/test-webcrypto-fips-exceptions.mjs diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index f6f82238b549..3297297abd59 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -20,6 +20,7 @@ const { normalizeHashName, numBitsToBytes, truncateToBitLength, + validateKmacKeyLength, } = require('internal/crypto/util'); const { @@ -60,6 +61,9 @@ function normalizeKeyLength(handle, algorithm) { length = algorithm.length; } + if (algorithm.name === 'KMAC128' || algorithm.name === 'KMAC256') + validateKmacKeyLength(length); + return { handle, length }; } diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 39ab1a387fb4..ebaf0bfd620c 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -50,10 +50,13 @@ const { EVP_PKEY_ML_KEM_1024, kKeyVariantAES_OCB_128: hasAesOcbMode, Argon2Job, + getFipsCrypto, getFipsCryptoGeneration, KmacJob, } = internalBinding('crypto'); +const isFips = getFipsCrypto() === 1; + const { getOptionValue } = require('internal/options'); const { @@ -480,6 +483,8 @@ const conditionalAlgorithms = { 'Ed448': !process.features.openssl_is_boringssl, 'KMAC128': !!KmacJob, 'KMAC256': !!KmacJob, + 'KT128': !isFips, + 'KT256': !isFips, 'ML-DSA-44': !!EVP_PKEY_ML_DSA_44, 'ML-DSA-65': !!EVP_PKEY_ML_DSA_65, 'ML-DSA-87': !!EVP_PKEY_ML_DSA_87, @@ -492,6 +497,8 @@ const conditionalAlgorithms = { ArrayPrototypeIncludes(getHashes(), 'sha3-384'), 'SHA3-512': !process.features.openssl_is_boringssl || ArrayPrototypeIncludes(getHashes(), 'sha3-512'), + 'TurboSHAKE128': !isFips, + 'TurboSHAKE256': !isFips, 'X448': !process.features.openssl_is_boringssl, }; @@ -636,6 +643,11 @@ function validateMaxBufferLength(data, name, max = kMaxBufferLength) { } } +function validateKmacKeyLength(length) { + if ((length < 32 || length % 8) && isFips) + throw lazyDOMException('Invalid key length', 'NotSupportedError'); +} + /** * Converts a bit length to the number of bytes needed to contain it. * Non-byte lengths are rounded up to the next byte. @@ -1141,6 +1153,7 @@ module.exports = { kNamedCurveAliases, kSupportedAlgorithms, + isFips, normalizeAlgorithm, normalizeHashName, hasAnyNotIn, @@ -1150,6 +1163,7 @@ module.exports = { jobPromiseThen, cleanupWebCryptoResult, prepareWebCryptoResult, + validateKmacKeyLength, validateMaxBufferLength, numBitsToBytes, truncateToBitLength, diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 08c02ac787d8..8e230004bf0a 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -9,7 +9,6 @@ const { StringPrototypeSplit, StringPrototypeStartsWith, StringPrototypeToLowerCase, - TypedArrayPrototypeGetLength, } = primordials; const { @@ -30,8 +29,10 @@ const { validateMaxBufferLength, getBufferSourceByteLength, getBufferSourceBytes, + isFips, kNamedCurveAliases, numBitsToBytes, + validateKmacKeyLength, } = require('internal/crypto/util'); const { converters: webidl, @@ -303,23 +304,24 @@ function validateCShakeOutputLength(V) { } } -function bufferSourceEqualsAscii(V, string) { - if (getBufferSourceByteLength(V) !== string.length) return false; - - const bytes = getBufferSourceBytes(V); - const length = TypedArrayPrototypeGetLength(bytes); - for (let i = 0; i < length; i++) { - if (bytes[i] !== StringPrototypeCharCodeAt(string, i)) return false; - } - return true; -} +const kCShakeFunctionNames = ['KMAC', 'TupleHash', 'ParallelHash']; function validateCShakeFunctionName(V) { - if (getBufferSourceByteLength(V) === 0 || - bufferSourceEqualsAscii(V, 'KMAC') || - bufferSourceEqualsAscii(V, 'TupleHash') || - bufferSourceEqualsAscii(V, 'ParallelHash')) { - return; + const length = getBufferSourceByteLength(V); + if (length === 0) return; + + if (!isFips) { + const bytes = getBufferSourceBytes(V); + for (let i = 0; i < kCShakeFunctionNames.length; i++) { + const functionName = kCShakeFunctionNames[i]; + if (length !== functionName.length) continue; + + let j = 0; + for (; j < length; j++) { + if (bytes[j] !== StringPrototypeCharCodeAt(functionName, j)) break; + } + if (j === length) return; + } } throw lazyDOMException( @@ -327,6 +329,14 @@ function validateCShakeFunctionName(V) { 'NotSupportedError'); } +function validateCShakeCustomization(V) { + if (isFips && getBufferSourceByteLength(V) !== 0) + throw lazyDOMException( + 'Unsupported CShakeParams customization', + 'NotSupportedError'); + validateMaxBufferLength(V, 'CShakeParams.customization', 512); +} + converters.RsaPssParams = createDictionaryConverter( 'RsaPssParams', [ dictAlgorithm, @@ -484,7 +494,7 @@ converters.CShakeParams = createDictionaryConverter( { key: 'customization', converter: converters.BufferSource, - validator: (V, opts) => validateMaxBufferLength(V, 'CShakeParams.customization', 512), + validator: validateCShakeCustomization, }, ], ]); @@ -782,6 +792,7 @@ for (let i = 0; i < kKmacDictionaries.length; i++) { key: 'length', converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), + validator: validateKmacKeyLength, }, ], ]); @@ -795,6 +806,12 @@ converters.KmacParams = createDictionaryConverter( key: 'outputLength', converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), + validator: (V) => { + if ((V === 0 || V % 8) && isFips) + throw lazyDOMException( + 'Invalid KmacParams outputLength', + 'NotSupportedError'); + }, required: true, }, { diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index c80658b61073..2cf971347b9e 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -1072,6 +1072,11 @@ Maybe CShakeTraits::AdditionalConfig( CShakeConfig* params) { Environment* env = Environment::GetCurrent(args); + if (IsFipsEnabled()) { + THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(env); + return Nothing(); + } + CHECK(args[offset]->IsString()); // Algorithm name Utf8Value algorithm_name(env->isolate(), args[offset]); std::string_view algorithm_str = algorithm_name.ToStringView(); diff --git a/src/crypto/crypto_kmac.cc b/src/crypto/crypto_kmac.cc index f728defee7cf..d346d50fc070 100644 --- a/src/crypto/crypto_kmac.cc +++ b/src/crypto/crypto_kmac.cc @@ -151,6 +151,8 @@ bool DeriveBitsWithCShake(const KmacConfig& params, const void* key_data, size_t key_size, ByteSource* out) { + if (IsFipsEnabled()) return false; + const size_t key_length_bytes = NumBitsToBytes(params.key_length); if (key_size < key_length_bytes) return false; diff --git a/src/crypto/crypto_turboshake.cc b/src/crypto/crypto_turboshake.cc index 899ed7c717dd..4078d213ddec 100644 --- a/src/crypto/crypto_turboshake.cc +++ b/src/crypto/crypto_turboshake.cc @@ -428,6 +428,11 @@ Maybe TurboShakeTraits::AdditionalConfig( TurboShakeConfig* params) { Environment* env = Environment::GetCurrent(args); + if (IsFipsEnabled()) { + THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(env); + return Nothing(); + } + // args[offset + 0] = algorithm name (string) CHECK(args[offset]->IsString()); Utf8Value algorithm_name(env->isolate(), args[offset]); @@ -533,6 +538,11 @@ Maybe KangarooTwelveTraits::AdditionalConfig( KangarooTwelveConfig* params) { Environment* env = Environment::GetCurrent(args); + if (IsFipsEnabled()) { + THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(env); + return Nothing(); + } + // args[offset + 0] = algorithm name (string) CHECK(args[offset]->IsString()); Utf8Value algorithm_name(env->isolate(), args[offset]); diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc index 80e06c66745a..37ce6c86485f 100644 --- a/src/crypto/crypto_util.cc +++ b/src/crypto/crypto_util.cc @@ -495,6 +495,11 @@ bool InitCryptoOnce(Isolate* isolate) { // be part of a larger mutex for global OpenSSL state. static Mutex fips_mutex; +bool IsFipsEnabled() { + Mutex::ScopedLock fips_lock(fips_mutex); + return ncrypto::isFipsEnabled(); +} + void InitCryptoOnce() { Mutex::ScopedLock lock(per_process::cli_options_mutex); Mutex::ScopedLock fips_lock(fips_mutex); @@ -557,8 +562,7 @@ void InitCryptoOnce() { void GetFipsCrypto(const FunctionCallbackInfo& args) { Mutex::ScopedLock lock(per_process::cli_options_mutex); - Mutex::ScopedLock fips_lock(fips_mutex); - args.GetReturnValue().Set(ncrypto::isFipsEnabled() ? 1 : 0); + args.GetReturnValue().Set(IsFipsEnabled() ? 1 : 0); } void GetFipsCryptoGeneration(const FunctionCallbackInfo& args) { diff --git a/src/crypto/crypto_util.h b/src/crypto/crypto_util.h index 0d4ff213adb8..faba0a348daf 100644 --- a/src/crypto/crypto_util.h +++ b/src/crypto/crypto_util.h @@ -68,6 +68,7 @@ constexpr T NumBitsToBytes(T bits) { // options were applied successfully. std::optional ProcessFipsOptions(); void InstallFipsIndicatorCallback(); +bool IsFipsEnabled(); bool InitCryptoOnce(v8::Isolate* isolate); void InitCryptoOnce(); diff --git a/test/parallel/test-crypto-key-objects-to-crypto-key.js b/test/parallel/test-crypto-key-objects-to-crypto-key.js index 3f74aaaee434..02e769bc3e57 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -14,6 +14,7 @@ const { } = require('crypto'); const { hasFIPS } = require('../common/crypto'); const { kSupportedAlgorithms } = require('internal/crypto/util'); +const fips = hasFIPS(); const rejectsXCurves = hasFIPS(3, 5); const hashes = Object.keys(kSupportedAlgorithms.digest).filter((name) => { @@ -135,7 +136,7 @@ function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { const key = createSecretKey(randomBytes(32)); const usages = ['sign', 'verify']; - if (allowZeroKey) { + if (allowZeroKey && !fips) { const zeroKey = createSecretKey(Buffer.alloc(0)) .toCryptoKey(algorithm, true, usages); assert.strictEqual(zeroKey.algorithm.length, 0); @@ -143,6 +144,16 @@ function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { const explicitZeroKey = createSecretKey(Buffer.alloc(0)) .toCryptoKey({ ...algorithm, length: 0 }, true, usages); assert.strictEqual(explicitZeroKey.algorithm.length, 0); + } else if (allowZeroKey) { + for (const zeroAlgorithm of [algorithm, { ...algorithm, length: 0 }]) { + assert.throws(() => { + createSecretKey(Buffer.alloc(0)) + .toCryptoKey(zeroAlgorithm, true, usages); + }, { + name: 'NotSupportedError', + message: 'Invalid key length', + }); + } } else { assert.throws(() => { createSecretKey(Buffer.alloc(0)).toCryptoKey(algorithm, true, usages); @@ -157,12 +168,15 @@ function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { message: 'Usages cannot be empty when importing a secret key.' }); - assert.throws(() => { - key.toCryptoKey({ ...algorithm, length: 0 }, true, usages); - }, { - name: 'DataError', - message: invalidLengthMessage, - }); + assert.throws( + () => key.toCryptoKey({ ...algorithm, length: 0 }, true, usages), + allowZeroKey && fips ? { + name: 'NotSupportedError', + message: 'Invalid key length', + } : { + name: 'DataError', + message: invalidLengthMessage, + }); } function hmacVectors() { diff --git a/test/parallel/test-webcrypto-derivekey.js b/test/parallel/test-webcrypto-derivekey.js index e3b4293983cc..dc3887b3d8fd 100644 --- a/test/parallel/test-webcrypto-derivekey.js +++ b/test/parallel/test-webcrypto-derivekey.js @@ -283,7 +283,7 @@ const fips4 = hasFIPS(4); })().then(common.mustCall()); } -if (hasOpenSSL(3)) { +if (hasOpenSSL(3) && !hasFIPS()) { (async () => { const derivedKeyAlgorithm = { name: 'KMAC128', length: 0 }; const usages = ['sign']; @@ -325,11 +325,7 @@ if (hasOpenSSL(3)) { name: 'KMAC128', outputLength: 256, }, derived, new Uint8Array()); - if (fips4) { - await assert.rejects(signature, { name: 'OperationError' }); - } else { - assert.strictEqual((await signature).byteLength, 32); - } + assert.strictEqual((await signature).byteLength, 32); } })().then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-digest-turboshake-rfc.js b/test/parallel/test-webcrypto-digest-turboshake-rfc.js index 271fde76ab23..462204654121 100644 --- a/test/parallel/test-webcrypto-digest-turboshake-rfc.js +++ b/test/parallel/test-webcrypto-digest-turboshake-rfc.js @@ -5,6 +5,11 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +const { hasFIPS } = require('../common/crypto'); + +if (hasFIPS()) + common.skip('TurboSHAKE and KangarooTwelve are not available in FIPS mode'); + const assert = require('assert'); const { subtle } = globalThis.crypto; diff --git a/test/parallel/test-webcrypto-digest-turboshake.js b/test/parallel/test-webcrypto-digest-turboshake.js index a6f4b2d50f94..bd09362caa25 100644 --- a/test/parallel/test-webcrypto-digest-turboshake.js +++ b/test/parallel/test-webcrypto-digest-turboshake.js @@ -5,6 +5,11 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +const { hasFIPS } = require('../common/crypto'); + +if (hasFIPS()) + common.skip('TurboSHAKE and KangarooTwelve are not available in FIPS mode'); + const assert = require('assert'); const { subtle } = globalThis.crypto; diff --git a/test/parallel/test-webcrypto-digest.js b/test/parallel/test-webcrypto-digest.js index 447948212bc1..7c0ef7668c61 100644 --- a/test/parallel/test-webcrypto-digest.js +++ b/test/parallel/test-webcrypto-digest.js @@ -10,7 +10,7 @@ const { Buffer } = require('buffer'); const { subtle } = globalThis.crypto; const { createHash, getHashes } = require('crypto'); const { hasOpenSSL, hasFIPS } = require('../common/crypto'); -const fips4 = hasFIPS(4); +const fips = hasFIPS(); const kTests = [ ['SHA-1', ['sha1'], 160], @@ -291,6 +291,8 @@ if (getHashes().includes('shake128')) { message: 'Unsupported CShakeParams functionName', }); + if (fips) return; + await assert.rejects( subtle.digest( { @@ -398,35 +400,19 @@ if (getHashes().includes('shake128')) { 'ca6f88db415829', }, ]) { - const digest = subtle.digest(algorithm, data); - if (fips4) { - await assert.rejects( - digest, - (err) => err.name === 'OperationError' && - err.cause?.code === 'ERR_OSSL_EVP_UNSUPPORTED'); - } else { - assert.strictEqual( - Buffer.from(await digest).toString('hex'), - expected); - } + assert.strictEqual( + Buffer.from(await subtle.digest(algorithm, data)).toString('hex'), + expected); } - const truncatedDigest = subtle.digest( + const truncated = Buffer.from(await subtle.digest( { ...nistCShakeSample1.algorithm, outputLength: 255 }, - nistCShakeSample1.data); - if (fips4) { - await assert.rejects( - truncatedDigest, - (err) => err.name === 'OperationError' && - err.cause?.code === 'ERR_OSSL_EVP_UNSUPPORTED'); - } else { - const truncated = Buffer.from(await truncatedDigest); - const expected = Buffer.from(nistCShakeSample1.expected, 'hex'); - assert.strictEqual(truncated.byteLength, expected.byteLength); - assert.deepStrictEqual( - truncated.subarray(0, 31), expected.subarray(0, 31)); - assert.strictEqual(truncated[31] & 0b00000001, 0); - assert.strictEqual(truncated[31] | 0b00000001, expected[31]); - } + nistCShakeSample1.data)); + const expected = Buffer.from(nistCShakeSample1.expected, 'hex'); + assert.strictEqual(truncated.byteLength, expected.byteLength); + assert.deepStrictEqual( + truncated.subarray(0, 31), expected.subarray(0, 31)); + assert.strictEqual(truncated[31] & 0b00000001, 0); + assert.strictEqual(truncated[31] | 0b00000001, expected[31]); })().then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-export-import.js b/test/parallel/test-webcrypto-export-import.js index 9f6b2125d8ed..30d1ea622fae 100644 --- a/test/parallel/test-webcrypto-export-import.js +++ b/test/parallel/test-webcrypto-export-import.js @@ -286,66 +286,68 @@ if (hasOpenSSL(3)) { { name: 'SyntaxError', message: 'Usages cannot be empty when importing a secret key.' }); { - const importedZeroImplicit = await subtle.importKey( - 'raw-secret', - new Uint8Array(), - name, - true, - ['sign', 'verify']); - const importedZeroImplicitRaw = - await subtle.exportKey('raw-secret', importedZeroImplicit); - assert.strictEqual(importedZeroImplicit.algorithm.length, 0); - assert.strictEqual(importedZeroImplicitRaw.byteLength, 0); + if (getFips() !== 1) { + const importedZeroImplicit = await subtle.importKey( + 'raw-secret', + new Uint8Array(), + name, + true, + ['sign', 'verify']); + const importedZeroImplicitRaw = + await subtle.exportKey('raw-secret', importedZeroImplicit); + assert.strictEqual(importedZeroImplicit.algorithm.length, 0); + assert.strictEqual(importedZeroImplicitRaw.byteLength, 0); - const importedZeroExplicit = await subtle.importKey( - 'raw-secret', - new Uint8Array(), - { name, length: 0 }, - true, - ['sign', 'verify']); - const importedZeroExplicitRaw = - await subtle.exportKey('raw-secret', importedZeroExplicit); - assert.strictEqual(importedZeroExplicit.algorithm.length, 0); - assert.strictEqual(importedZeroExplicitRaw.byteLength, 0); - - await assert.rejects( - subtle.importKey( + const importedZeroExplicit = await subtle.importKey( 'raw-secret', - new Uint8Array([0xff]), + new Uint8Array(), { name, length: 0 }, true, - ['sign', 'verify']), - { name: 'DataError', message: 'Invalid key length' }); - - const generated = await subtle.generateKey( - { name, length: 9 }, - true, - ['sign', 'verify']); - const generatedRaw = await subtle.exportKey('raw-secret', generated); - assert.strictEqual(generated.algorithm.length, 9); - assert.strictEqual(generatedRaw.byteLength, 2); - assert.strictEqual(new Uint8Array(generatedRaw)[1] & 0b01111111, 0); + ['sign', 'verify']); + const importedZeroExplicitRaw = + await subtle.exportKey('raw-secret', importedZeroExplicit); + assert.strictEqual(importedZeroExplicit.algorithm.length, 0); + assert.strictEqual(importedZeroExplicitRaw.byteLength, 0); + + await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array([0xff]), + { name, length: 0 }, + true, + ['sign', 'verify']), + { name: 'DataError', message: 'Invalid key length' }); + + const generated = await subtle.generateKey( + { name, length: 9 }, + true, + ['sign', 'verify']); + const generatedRaw = await subtle.exportKey('raw-secret', generated); + assert.strictEqual(generated.algorithm.length, 9); + assert.strictEqual(generatedRaw.byteLength, 2); + assert.strictEqual(new Uint8Array(generatedRaw)[1] & 0b01111111, 0); - const importedExplicit = await subtle.importKey( - 'raw-secret', - new Uint8Array([0xff, 0xff]), - { name, length: 9 }, - true, - ['sign', 'verify']); - const importedExplicitRaw = await subtle.exportKey('raw-secret', importedExplicit); - assert.strictEqual(importedExplicit.algorithm.length, 9); - assert.deepStrictEqual( - new Uint8Array(importedExplicitRaw), - new Uint8Array([0xff, 0x80])); - - await assert.rejects( - subtle.importKey( + const importedExplicit = await subtle.importKey( 'raw-secret', - new Uint8Array([0xff]), + new Uint8Array([0xff, 0xff]), { name, length: 9 }, true, - ['sign', 'verify']), - { name: 'DataError', message: 'Invalid key length' }); + ['sign', 'verify']); + const importedExplicitRaw = await subtle.exportKey('raw-secret', importedExplicit); + assert.strictEqual(importedExplicit.algorithm.length, 9); + assert.deepStrictEqual( + new Uint8Array(importedExplicitRaw), + new Uint8Array([0xff, 0x80])); + + await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array([0xff]), + { name, length: 9 }, + true, + ['sign', 'verify']), + { name: 'DataError', message: 'Invalid key length' }); + } } } diff --git a/test/parallel/test-webcrypto-fips-exceptions.mjs b/test/parallel/test-webcrypto-fips-exceptions.mjs new file mode 100644 index 000000000000..d9c26a1674d6 --- /dev/null +++ b/test/parallel/test-webcrypto-fips-exceptions.mjs @@ -0,0 +1,199 @@ +// Flags: --expose-internals + +import * as common from '../common/index.mjs'; +import assert from 'node:assert'; +import { createRequire } from 'node:module'; +import { hasFIPS } from '../common/crypto.js'; + +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!hasFIPS(3)) + common.skip('requires OpenSSL >= 3 in FIPS mode'); + +const require = createRequire(import.meta.url); +const { internalBinding } = require('internal/test/binding'); +const { getCryptoKeyHandle } = require('internal/crypto/keys'); +const { + CShakeJob, + KangarooTwelveJob, + KmacJob, + TurboShakeJob, + kCryptoJobWebCrypto, + kSignJobModeSign, +} = internalBinding('crypto'); +const { subtle } = globalThis.crypto; +const { SubtleCrypto } = globalThis; +const data = new Uint8Array(); + +async function assertFipsException(operation, algorithm, fn, message) { + assert.strictEqual(SubtleCrypto.supports(operation, algorithm), false); + await assert.rejects(fn(), { + name: 'NotSupportedError', + message, + }); +} + +for (const algorithm of [ + { name: 'turboshake128', outputLength: 128 }, + { name: 'TurboSHAKE256', outputLength: 256 }, + { name: 'KT128', outputLength: 128 }, + { name: 'KT256', outputLength: 256, customization: data }, +]) { + await assertFipsException( + 'digest', + algorithm, + () => subtle.digest(algorithm, data), + 'Unrecognized algorithm name'); +} + +for (const createJob of [ + () => new TurboShakeJob( + kCryptoJobWebCrypto, 'TurboSHAKE128', 0x1f, 16, data), + () => new KangarooTwelveJob( + kCryptoJobWebCrypto, 'KT128', undefined, 16, data), + () => new CShakeJob( + kCryptoJobWebCrypto, + 'cSHAKE128', + data, + Buffer.from('KMAC'), + undefined, + 128), +]) { + assert.throws(createJob, { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + message: 'Unsupported crypto operation', + }); +} + +const emptyCShake = { + name: 'cSHAKE128', + outputLength: 256, + customization: data, + functionName: data, +}; +assert.strictEqual(SubtleCrypto.supports('digest', emptyCShake), true); + +for (const length of [1, 513]) { + const algorithm = { + name: 'cSHAKE128', + outputLength: 256, + customization: new Uint8Array(length), + }; + await assertFipsException( + 'digest', + algorithm, + () => subtle.digest(algorithm, data), + 'Unsupported CShakeParams customization'); +} + +const functionName = { + name: 'cSHAKE256', + outputLength: 256, + functionName: Buffer.from('KMAC'), +}; +await assertFipsException( + 'digest', + functionName, + () => subtle.digest(functionName, data), + 'Unsupported CShakeParams functionName'); + +const bothCShakeParams = { + ...functionName, + customization: new Uint8Array(1), +}; +await assertFipsException( + 'digest', + bothCShakeParams, + () => subtle.digest(bothCShakeParams, data), + 'Unsupported CShakeParams customization'); + +for (const length of [0, 24, 33]) { + const algorithm = { name: 'KMAC128', length }; + await assertFipsException( + 'generateKey', + algorithm, + () => subtle.generateKey(algorithm, false, ['sign', 'verify']), + 'Invalid key length'); + await assertFipsException( + 'importKey', + algorithm, + () => subtle.importKey( + 'raw-secret', + new Uint8Array(length === 24 ? 4 : Math.ceil(length / 8)), + algorithm, + false, + ['sign', 'verify']), + 'Invalid key length'); +} + +const minimumKmac = { name: 'KMAC128', length: 32 }; +assert.strictEqual( + SubtleCrypto.supports('generateKey', minimumKmac), true); +assert.strictEqual( + SubtleCrypto.supports('importKey', minimumKmac), true); +await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array(5), + minimumKmac, + false, + ['sign', 'verify']), { + name: 'DataError', + message: 'Invalid key length', + }); + +for (const length of [0, 3]) { + await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array(length), + 'KMAC128', + false, + ['sign', 'verify']), { + name: 'NotSupportedError', + message: 'Invalid key length', + }); +} +const key = await subtle.importKey( + 'raw-secret', + new Uint8Array(4), + 'KMAC128', + false, + ['sign', 'verify']); +assert.strictEqual(key.algorithm.length, 32); + +await assert.rejects( + new KmacJob( + kCryptoJobWebCrypto, + kSignJobModeSign, + getCryptoKeyHandle(key), + 'KMAC128', + undefined, + 32, + 9, + data, + undefined).run(), + (err) => { + assert.strictEqual(err.name, 'OperationError'); + assert.strictEqual(err.cause?.name, 'Error'); + assert.strictEqual(err.cause?.message, 'Deriving bits failed'); + return true; + }); + +const minimumOutput = { name: 'KMAC128', outputLength: 8 }; +assert.strictEqual(SubtleCrypto.supports('sign', minimumOutput), true); +assert.strictEqual(SubtleCrypto.supports('verify', minimumOutput), true); +for (const outputLength of [0, 9]) { + const algorithm = { name: 'KMAC128', outputLength }; + await assertFipsException( + 'sign', + algorithm, + () => subtle.sign(algorithm, key, data), + 'Invalid KmacParams outputLength'); + await assertFipsException( + 'verify', + algorithm, + () => subtle.verify(algorithm, key, data, data), + 'Invalid KmacParams outputLength'); +} diff --git a/test/parallel/test-webcrypto-keygen-kmac.js b/test/parallel/test-webcrypto-keygen-kmac.js index c1125412892e..33716095751f 100644 --- a/test/parallel/test-webcrypto-keygen-kmac.js +++ b/test/parallel/test-webcrypto-keygen-kmac.js @@ -5,7 +5,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { hasFIPS, hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3)) common.skip('requires OpenSSL >= 3'); @@ -13,38 +13,45 @@ if (!hasOpenSSL(3)) const assert = require('assert'); const { types: { isCryptoKey } } = require('util'); const { subtle } = globalThis.crypto; +const fips = hasFIPS(); const usages = ['sign', 'verify']; async function test(name, length) { - length ??= name === 'KMAC128' ? 128 : 256; - const key = await subtle.generateKey({ - name, - length, - }, true, usages); - - assert(key); - assert(isCryptoKey(key)); - - assert.strictEqual(key.type, 'secret'); - assert.strictEqual(key.toString(), '[object CryptoKey]'); - assert.strictEqual(key.extractable, true); - assert.deepStrictEqual(key.usages, usages); - assert.strictEqual(key.algorithm.name, name); - assert.strictEqual(key.algorithm.length, length); - assert.strictEqual(key.algorithm, key.algorithm); - assert.strictEqual(key.usages, key.usages); - - const raw = await subtle.exportKey('raw-secret', key); - assert.strictEqual(raw.byteLength, Math.ceil(length / 8)); + const expectedLength = length ?? (name === 'KMAC128' ? 128 : 256); + const algorithm = { name }; + if (length !== undefined) + algorithm.length = length; + + if (fips && length !== undefined && + (length < 32 || length % 8 !== 0)) return; + + const generatedKey = await subtle.generateKey(algorithm, true, usages); + + assert(generatedKey); + assert(isCryptoKey(generatedKey)); + + assert.strictEqual(generatedKey.type, 'secret'); + assert.strictEqual(generatedKey.toString(), '[object CryptoKey]'); + assert.strictEqual(generatedKey.extractable, true); + assert.deepStrictEqual(generatedKey.usages, usages); + assert.strictEqual(generatedKey.algorithm.name, name); + assert.strictEqual(generatedKey.algorithm.length, expectedLength); + assert.strictEqual(generatedKey.algorithm, generatedKey.algorithm); + assert.strictEqual(generatedKey.usages, generatedKey.usages); + + const raw = await subtle.exportKey('raw-secret', generatedKey); + assert.strictEqual(raw.byteLength, Math.ceil(expectedLength / 8)); } const kTests = [ ['KMAC128', 0], + ['KMAC128', 32], ['KMAC128', 128], ['KMAC128', 256], ['KMAC128'], ['KMAC256', 0], + ['KMAC256', 32], ['KMAC256', 128], ['KMAC256', 256], ['KMAC256'], diff --git a/test/parallel/test-webcrypto-prototype-pollution.mjs b/test/parallel/test-webcrypto-prototype-pollution.mjs index a7104c2b7ae4..5ed77fc6021e 100644 --- a/test/parallel/test-webcrypto-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-prototype-pollution.mjs @@ -142,16 +142,24 @@ if (supports('digest', 'cSHAKE128')) { outputLength: 256, customization: new Uint8Array([1, 2, 3]), }; - const expected = new Uint8Array(await subtle.digest(algorithm, data)); - const plain = new Uint8Array( - await subtle.digest({ name: 'cSHAKE128', outputLength: 256 }, data)); - assert.notDeepStrictEqual(expected, plain); - await withPoisoned(poisonTypedArrayByteLength(0), - common.mustCall(async () => { - assert.deepStrictEqual( - new Uint8Array(await subtle.digest(algorithm, data)), - expected); - })); + if (getFips() === 1) { + await withPoisoned(poisonTypedArrayByteLength(0), common.mustCall(() => + assert.rejects(subtle.digest(algorithm, data), { + name: 'NotSupportedError', + message: 'Unsupported CShakeParams customization', + }))); + } else { + const expected = new Uint8Array(await subtle.digest(algorithm, data)); + const plain = new Uint8Array( + await subtle.digest({ name: 'cSHAKE128', outputLength: 256 }, data)); + assert.notDeepStrictEqual(expected, plain); + await withPoisoned(poisonTypedArrayByteLength(0), + common.mustCall(async () => { + assert.deepStrictEqual( + new Uint8Array(await subtle.digest(algorithm, data)), + expected); + })); + } } } @@ -290,17 +298,17 @@ await withPoisoned( // enforceRangeOptions(): [EnforceRange] uses IntegerPart, not round-half-even. { const key = await subtle.importKey( - 'raw-secret', new Uint8Array(4), 'PBKDF2', false, ['deriveBits']); + 'raw-secret', new Uint8Array(32), 'PBKDF2', false, ['deriveBits']); const pbkdf2 = (iterations) => subtle.deriveBits({ name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array(16), iterations, - }, key, 8); + }, key, 112); - const expected = new Uint8Array(await pbkdf2(1)); + const expected = new Uint8Array(await pbkdf2(1000)); await withPoisoned(inherited('clamp', true), common.mustCall(async () => { - assert.deepStrictEqual(new Uint8Array(await pbkdf2(1.5)), expected); + assert.deepStrictEqual(new Uint8Array(await pbkdf2(1000.5)), expected); })); } diff --git a/test/parallel/test-webcrypto-sign-verify-kmac.js b/test/parallel/test-webcrypto-sign-verify-kmac.js index 160067b9b760..ac0b738bcd57 100644 --- a/test/parallel/test-webcrypto-sign-verify-kmac.js +++ b/test/parallel/test-webcrypto-sign-verify-kmac.js @@ -12,15 +12,24 @@ if (!hasOpenSSL(3)) const assert = require('assert'); const { subtle } = globalThis.crypto; +const fips = hasFIPS(); const fips4 = hasFIPS(4); const vectors = require('../fixtures/crypto/kmac')(); -function isFipsUnsupported(err) { +function isFipsProviderUnsupported(err) { return err.name === 'OperationError' && err.cause?.code === 'ERR_OSSL_EVP_UNSUPPORTED'; } +function usesNonFipsImplementation({ key, keyLength, outputLength }) { + const keyLengthInBits = keyLength ?? key.byteLength * 8; + return outputLength === 0 || + outputLength % 8 !== 0 || + keyLengthInBits < 32 || + keyLengthInBits % 8 !== 0; +} + function isFips4Incompatible({ key, keyLength, outputLength }) { const keyLengthInBits = keyLength ?? key.byteLength * 8; return keyLengthInBits < 128 || @@ -206,9 +215,13 @@ async function testSign({ algorithm, const variations = []; for (const vector of vectors) { + if (fips && usesNonFipsImplementation(vector)) continue; + if (fips4 && isFips4Incompatible(vector)) { - variations.push(assert.rejects(testVerify(vector), isFipsUnsupported)); - variations.push(assert.rejects(testSign(vector), isFipsUnsupported)); + variations.push(assert.rejects( + testVerify(vector), isFipsProviderUnsupported)); + variations.push(assert.rejects( + testSign(vector), isFipsProviderUnsupported)); } else { variations.push(testVerify(vector)); variations.push(testSign(vector)); @@ -227,24 +240,18 @@ async function testSign({ algorithm, ['sign', 'verify']); const algorithm = { name: 'KMAC128', - outputLength: fips4 ? 16 : 9, + outputLength: fips ? 16 : 9, customization: new Uint8Array(), }; const data = new Uint8Array([1, 2, 3]); - if (fips4) { - await assert.rejects( - subtle.sign({ ...algorithm, outputLength: 9 }, key, data), - isFipsUnsupported); - } - const signature = await subtle.sign(algorithm, key, data); assert.strictEqual(signature.byteLength, 2); - if (!fips4) + if (!fips) assert.strictEqual(new Uint8Array(signature)[1] & 0b01111111, 0); assert(await subtle.verify(algorithm, key, signature, data)); - if (fips4) { + if (fips) { const signature128 = await subtle.sign({ ...algorithm, outputLength: 128, @@ -264,25 +271,23 @@ async function testSign({ algorithm, } const invalidSignature = new Uint8Array(signature); - if (fips4) + if (fips) invalidSignature[0] ^= 0b00000001; else invalidSignature[1] |= 0b00000001; assert(!(await subtle.verify(algorithm, key, invalidSignature, data))); - const nonByteKey = await subtle.importKey( - 'raw-secret', - new Uint8Array([0xff, 0xff, 0xff, 0xff]), - { name: 'KMAC128', length: 25 }, - false, - ['sign', 'verify']); - const nonByteKeySignature = subtle.sign({ - ...algorithm, - outputLength: 16, - }, nonByteKey, data); - if (fips4) { - await assert.rejects(nonByteKeySignature, isFipsUnsupported); - } else { + if (!fips) { + const nonByteKey = await subtle.importKey( + 'raw-secret', + new Uint8Array([0xff, 0xff, 0xff, 0xff]), + { name: 'KMAC128', length: 25 }, + false, + ['sign', 'verify']); + const nonByteKeySignature = subtle.sign({ + ...algorithm, + outputLength: 16, + }, nonByteKey, data); const result = await nonByteKeySignature; assert.strictEqual(result.byteLength, 2); assert(await subtle.verify({ @@ -293,6 +298,8 @@ async function testSign({ algorithm, })().then(common.mustCall()); (async function() { + if (fips) return; + const data = new Uint8Array([1, 2, 3]); for (const name of ['KMAC128', 'KMAC256']) { @@ -311,13 +318,9 @@ async function testSign({ algorithm, const algorithm = { name, outputLength: 256 }; const signature = subtle.sign(algorithm, key, data); - if (fips4) { - await assert.rejects(signature, isFipsUnsupported); - } else { - const result = await signature; - assert.strictEqual(result.byteLength, 32); - assert(await subtle.verify(algorithm, key, result, data)); - } + const result = await signature; + assert.strictEqual(result.byteLength, 32); + assert(await subtle.verify(algorithm, key, result, data)); } } })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index 9695ee03298d..644afd712ef9 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -485,7 +485,7 @@ async function testNonByteLengthWrapUnwrap({ implicitAlgorithm: hmacAlgorithm, }); - if (hasOpenSSL(3)) { + if (hasOpenSSL(3) && getFips() !== 1) { const kmacAlgorithm = { name: 'KMAC128' }; const kmacKey = await subtle.importKey( 'raw-secret', diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index 33e10d491c15..0c492568c32f 100644 --- a/test/wpt/status/WebCryptoAPI.cjs +++ b/test/wpt/status/WebCryptoAPI.cjs @@ -120,6 +120,12 @@ if (hasFIPS(3)) { ]); } +if (hasFIPS()) { + skip( + 'digest/kangarootwelve.tentative.https.any.js', + 'digest/turboshake.tentative.https.any.js'); +} + // OpenSSL 3.0 through 3.3 reject SHA-1 signature generation in FIPS mode. // OpenSSL 3.4 permits it for legacy use cases while marking the operation as // non-approved through a per-operation FIPS indicator. Node does not expose @@ -174,8 +180,10 @@ if (hasFIPS(4)) { ]); } -skipSubtests( - ['digest/kangarootwelve.tentative.https.any.js', /C=(?:\d{4,}|5(?:1[3-9]|[2-9]\d)|[6-9]\d{2}) bytes/]); +if (!hasFIPS()) { + skipSubtests( + ['digest/kangarootwelve.tentative.https.any.js', /C=(?:\d{4,}|5(?:1[3-9]|[2-9]\d)|[6-9]\d{2}) bytes/]); +} function assertNoOverlap(fileSkips, subtestSkips) { const subtestSkipFiles = new Set(Object.keys(subtestSkips)); From b7d42af10ef2bb493f61f2cfb8a381c0fd8d504b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 8 Sep 2026 10:50:21 +0200 Subject: [PATCH 03/22] lib: use Web IDL interface brand checks Use explicit brand predicates for interface conversion instead of prototype ancestry. Update CryptoKey and AbortSignal together with the shared converter contract. Read internal AbortSignal state during composition. Preserve genuine signals after prototype changes without invoking shadowed getters. Signed-off-by: Filip Skokan Assisted-by: GitHub Copilot PR-URL: https://github.com/nodejs/node/pull/65846 Reviewed-By: James M Snell Reviewed-By: Aviv Keller --- lib/internal/abort_controller.js | 31 ++++++------ lib/internal/crypto/webidl.js | 4 +- lib/internal/webidl.js | 7 ++- test/parallel/test-internal-webidl.js | 48 +++++++++++++++++-- .../test-webcrypto-cryptokey-brand-check.js | 5 +- test/parallel/test-webcrypto-webidl-brand.js | 38 +++++++++++++++ 6 files changed, 108 insertions(+), 25 deletions(-) create mode 100644 test/parallel/test-webcrypto-webidl-brand.js diff --git a/lib/internal/abort_controller.js b/lib/internal/abort_controller.js index 3352f6a4cff5..5672b6b80f6c 100644 --- a/lib/internal/abort_controller.js +++ b/lib/internal/abort_controller.js @@ -149,8 +149,8 @@ function refreshCompositeSignal(signal) { continue; } - if (sourceSignal.aborted) { - abortSignal(signal, sourceSignal.reason); + if (sourceSignal[kAborted]) { + abortSignal(signal, sourceSignal[kReason]); return; } } @@ -170,8 +170,8 @@ function followCompositeSignal(signal) { continue; } - if (sourceSignal.aborted) { - abortSignal(signal, sourceSignal.reason); + if (sourceSignal[kAborted]) { + abortSignal(signal, sourceSignal[kReason]); return; } @@ -217,6 +217,14 @@ function setWeakAbortSignalTimeout(weakRef, delay) { } class AbortSignal extends EventTarget { + #brand; + + static { + converters.AbortSignal = createInterfaceConverter( + 'AbortSignal', + (value) => typeof value === 'object' && value !== null && #brand in value, + ); + } /** * @param {symbol | undefined} dontThrowSymbol @@ -337,8 +345,9 @@ class AbortSignal extends EventTarget { gcPersistentSignals.add(signal); } - if (signal.aborted) { - abortSignal(resultSignal, signal.reason); + refreshCompositeSignal(signal); + if (signal[kAborted]) { + abortSignal(resultSignal, signal[kReason]); return resultSignal; } @@ -348,11 +357,6 @@ class AbortSignal extends EventTarget { } else if (!signal[kSourceSignals]) { continue; } else { - refreshCompositeSignal(signal); - if (signal.aborted) { - abortSignal(resultSignal, signal.reason); - return resultSignal; - } for (const sourceSignalWeakRef of signal[kSourceSignals]) { const sourceSignal = sourceSignalWeakRef.deref(); if (!sourceSignal) { @@ -360,8 +364,8 @@ class AbortSignal extends EventTarget { } assert(!sourceSignal[kComposite]); - if (sourceSignal.aborted) { - abortSignal(resultSignal, sourceSignal.reason); + if (sourceSignal[kAborted]) { + abortSignal(resultSignal, sourceSignal[kReason]); return resultSignal; } @@ -470,7 +474,6 @@ class AbortSignal extends EventTarget { } } -converters.AbortSignal = createInterfaceConverter('AbortSignal', AbortSignal.prototype); converters['sequence'] = createSequenceConverter(converters.AbortSignal); function ClonedAbortSignal() { diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 8e230004bf0a..dc8a1e6d72b3 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -19,10 +19,10 @@ const { isInt32, isUint32, } = require('internal/validators'); -const { CryptoKey } = require('internal/crypto/webcrypto'); const { getCryptoKeyAlgorithm, getCryptoKeyType, + isCryptoKey, } = require('internal/crypto/keys'); const { bigIntArrayToUnsignedInt, @@ -649,7 +649,7 @@ converters.AesCtrParams = createDictionaryConverter( ]); converters.CryptoKey = createInterfaceConverter( - 'CryptoKey', CryptoKey.prototype); + 'CryptoKey', isCryptoKey); converters.EcdhKeyDeriveParams = createDictionaryConverter( 'EcdhKeyDeriveParams', [ diff --git a/lib/internal/webidl.js b/lib/internal/webidl.js index 95f941d364b4..fc07e790ba55 100644 --- a/lib/internal/webidl.js +++ b/lib/internal/webidl.js @@ -21,7 +21,6 @@ const { NumberMAX_SAFE_INTEGER, NumberMIN_SAFE_INTEGER, ObjectPrototypeHasOwnProperty, - ObjectPrototypeIsPrototypeOf, SafeArrayIterator, SafeSet, String, @@ -863,13 +862,13 @@ function createSequenceConverter(converter) { * Creates a converter for a Web IDL interface type. * @see https://webidl.spec.whatwg.org/#js-interface * @param {string} name Interface identifier. - * @param {object} prototype Interface prototype object. + * @param {(value: any) => boolean} brandCheck Interface brand predicate. * @returns {Converter} */ -function createInterfaceConverter(name, prototype) { +function createInterfaceConverter(name, brandCheck) { return (V, options = kEmptyObject) => { // Web IDL interface conversion step 1: return V if it implements I. - if (ObjectPrototypeIsPrototypeOf(prototype, V)) { + if (brandCheck(V)) { return V; } // Step 2: otherwise throw. diff --git a/test/parallel/test-internal-webidl.js b/test/parallel/test-internal-webidl.js index 5084d80efd93..f52896d5210f 100644 --- a/test/parallel/test-internal-webidl.js +++ b/test/parallel/test-internal-webidl.js @@ -1,7 +1,7 @@ // Flags: --expose-internals 'use strict'; -require('../common'); +const common = require('../common'); const assert = require('assert'); const vm = require('vm'); const webidl = require('internal/webidl'); @@ -544,10 +544,16 @@ for (const [prototype, value] of [ } { - class Example {} + class Example { + #brand; + + static is(value) { + return typeof value === 'object' && value !== null && #brand in value; + } + } const converter = webidl.createInterfaceConverter( 'Example', - Example.prototype); + Example.is); const example = new Example(); assert.strictEqual(converter(example), example); @@ -556,6 +562,42 @@ for (const [prototype, value] of [ code: 'ERR_INVALID_ARG_TYPE', message: 'Prefix: Context is not of type Example.', }); + assertInvalidArgType(() => converter({ __proto__: Example.prototype })); + assertInvalidArgType(() => converter(new Proxy(example, {}))); + Object.setPrototypeOf(example, null); + assert.strictEqual(converter(example), example); +} + +{ + const signal = AbortSignal.abort('reason'); + for (const value of [ + Object.create(AbortSignal.prototype, { aborted: { value: false } }), + { __proto__: signal }, + Object.create(AbortSignal.prototype, Object.getOwnPropertyDescriptors(signal)), + new Proxy(signal, {}), + ]) { + assertInvalidArgType(() => converters.AbortSignal(value)); + assertInvalidArgType(() => AbortSignal.any([value])); + } + + Object.setPrototypeOf(signal, null); + assert.strictEqual(converters.AbortSignal(signal), signal); + const composite = AbortSignal.any([signal]); + assert.strictEqual(composite.aborted, true); + assert.strictEqual(composite.reason, 'reason'); +} + +{ + const controller = new AbortController(); + Object.defineProperties(controller.signal, { + aborted: { get: common.mustNotCall('Unexpected aborted getter') }, + reason: { get: common.mustNotCall('Unexpected reason getter') }, + }); + const composite = AbortSignal.any([controller.signal]); + assert.strictEqual(composite.aborted, false); + controller.abort('reason'); + assert.strictEqual(composite.aborted, true); + assert.strictEqual(composite.reason, 'reason'); } { diff --git a/test/parallel/test-webcrypto-cryptokey-brand-check.js b/test/parallel/test-webcrypto-cryptokey-brand-check.js index 3fe8aaa181a2..9dd115f00721 100644 --- a/test/parallel/test-webcrypto-cryptokey-brand-check.js +++ b/test/parallel/test-webcrypto-cryptokey-brand-check.js @@ -54,6 +54,7 @@ const { subtle } = globalThis.crypto; assert.strictEqual(Object.getPrototypeOf(internalProto), CryptoKey.prototype); const invalidThis = { code: 'ERR_INVALID_THIS', name: 'TypeError' }; + const invalidArgType = { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' }; // Plain object receiver. Object.entries(getters).forEach(([, getter]) => { @@ -94,10 +95,10 @@ const { subtle } = globalThis.crypto; assert.strictEqual(isCryptoKey(spoofed), false); await assert.rejects( subtle.sign('HMAC', spoofed, Buffer.from('payload')), - invalidThis); + invalidArgType); await assert.rejects( subtle.exportKey('jwk', spoofed), - invalidThis); + invalidArgType); // Subvert `instanceof CryptoKey` via Symbol.hasInstance, then // invoke the native getters on a forged object. The C++ tag diff --git a/test/parallel/test-webcrypto-webidl-brand.js b/test/parallel/test-webcrypto-webidl-brand.js new file mode 100644 index 000000000000..d4d785684906 --- /dev/null +++ b/test/parallel/test-webcrypto-webidl-brand.js @@ -0,0 +1,38 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; +const { CryptoKey } = require('internal/crypto/keys'); +const { converters } = require('internal/crypto/webidl'); + +async function main() { + const bytes = new Uint8Array(16); + const key = await subtle.importKey('raw', bytes, 'AES-GCM', true, ['encrypt']); + + for (const value of [ + { __proto__: CryptoKey.prototype }, + { __proto__: key }, + Object.create(CryptoKey.prototype, Object.getOwnPropertyDescriptors(key)), + new Proxy(key, {}), + ]) { + assert.throws(() => converters.CryptoKey(value), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + }); + await assert.rejects(subtle.exportKey('raw', value), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + }); + } + + Object.setPrototypeOf(key, null); + assert.strictEqual(converters.CryptoKey(key), key); + assert.deepStrictEqual(new Uint8Array(await subtle.exportKey('raw', key)), bytes); +} + +main().then(common.mustCall()); From 936824f1a31f2058bdb729703fc31f1d02e0f285 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 9 Sep 2026 15:57:36 +0200 Subject: [PATCH 04/22] lib: fix shared buffer growability validation Use the intrinsic growable getter instead of buffer.growable so shadowed properties cannot bypass validation or reject fixed buffers. Signed-off-by: Filip Skokan Assisted-by: GitHub Copilot PR-URL: https://github.com/nodejs/node/pull/65845 Reviewed-By: James M Snell Reviewed-By: Yagiz Nizipli --- lib/internal/webidl.js | 10 +-- src/node_util.cc | 19 ++++ .../test-internal-webidl-buffer-source.js | 90 ++++++++++++++++++- typings/internalBinding/util.d.ts | 1 + 4 files changed, 112 insertions(+), 8 deletions(-) diff --git a/lib/internal/webidl.js b/lib/internal/webidl.js index fc07e790ba55..446a4e5f5aec 100644 --- a/lib/internal/webidl.js +++ b/lib/internal/webidl.js @@ -36,6 +36,8 @@ const { isTypedArray, } = require('internal/util/types'); +const { getSharedArrayBufferGrowable } = internalBinding('util'); + const BIGINT_2_63 = 1n << 63n; const BIGINT_2_64 = 1n << 64n; @@ -934,12 +936,8 @@ function validateBufferSourceBacking(buffer, options) { function validateAllowGrowableSharedArrayBuffer(buffer, options) { // SharedArrayBuffer and ArrayBufferView conversion step 3: // IsFixedLengthArrayBuffer(buffer) must be true without [AllowResizable]. - // Do not use a primordial getter here. When this module is included in the - // startup snapshot, an early-captured SharedArrayBuffer.prototype.growable - // getter does not detect growable buffers created after deserialization. - // Lazily capturing the getter would work, but it would observe the runtime - // prototype at first comparison, so it would not be an actual primordial. - if (!options.allowResizable && buffer.growable) { + if (!options.allowResizable && + FunctionPrototypeCall(getSharedArrayBufferGrowable, buffer)) { throw makeException( 'is backed by a growable SharedArrayBuffer, which is not allowed.', options); diff --git a/src/node_util.cc b/src/node_util.cc index c839fdd1f2fc..96c0c9397546 100644 --- a/src/node_util.cc +++ b/src/node_util.cc @@ -520,6 +520,25 @@ void Initialize(Local target, Environment* env = Environment::GetCurrent(context); Isolate* isolate = env->isolate(); + { + const Local prototype = + SharedArrayBuffer::New(isolate, 0)->GetPrototypeV2().As(); + const Local descriptor = + prototype + ->GetOwnPropertyDescriptor( + context, FIXED_ONE_BYTE_STRING(isolate, "growable")) + .ToLocalChecked() + .As(); + const Local getter = + descriptor->Get(context, env->get_string()).ToLocalChecked(); + CHECK(getter->IsFunction()); + target + ->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "getSharedArrayBufferGrowable"), + getter) + .Check(); + } + { Local tmpl = ObjectTemplate::New(isolate); #define V(PropertyName, _) \ diff --git a/test/parallel/test-internal-webidl-buffer-source.js b/test/parallel/test-internal-webidl-buffer-source.js index 9e522d7d7b8a..b3158bd7d348 100644 --- a/test/parallel/test-internal-webidl-buffer-source.js +++ b/test/parallel/test-internal-webidl-buffer-source.js @@ -1,7 +1,7 @@ -// Flags: --expose-internals +// Flags: --expose-internals --experimental-wasm-rab-integration 'use strict'; -require('../common'); +const common = require('../common'); const assert = require('assert'); const { test } = require('node:test'); const vm = require('vm'); @@ -272,6 +272,92 @@ test('AllowSharedBufferSource handles growable shared buffers with explicit ' + } }); +test('Shared buffer growability checks do not read JavaScript properties', () => { + for (const [buffer, growable] of [ + [new SharedArrayBuffer(8), false], + [new SharedArrayBuffer(8, { maxByteLength: 8 }), true], + [new SharedArrayBuffer(8, { maxByteLength: 16 }), true], + [vm.runInNewContext('new SharedArrayBuffer(8)'), false], + [vm.runInNewContext('new SharedArrayBuffer(8, { maxByteLength: 16 })'), true], + ]) { + const view = new Uint8Array(buffer); + const dataView = new DataView(buffer); + for (const mode of ['shadow', 'getter', 'prototype']) { + if (mode === 'shadow') { + Object.defineProperty(buffer, 'growable', { + value: !growable, + configurable: true, + }); + } else if (mode === 'getter') { + Object.defineProperty(buffer, 'growable', { + get: common.mustNotCall('Unexpected growable getter'), + configurable: true, + }); + } else { + delete buffer.growable; + Object.setPrototypeOf(buffer, null); + } + + for (const value of [buffer, view, dataView]) { + if (growable) { + assert.throws(() => converters.AllowSharedBufferSource(value), { + code: 'ERR_INVALID_ARG_TYPE', + }); + } else { + assert.strictEqual(converters.AllowSharedBufferSource(value), value); + } + assert.strictEqual(converters.AllowSharedBufferSource(value, { + allowResizable: true, + }), value); + } + + if (growable) { + assert.throws(() => converters.Uint8Array(view, { allowShared: true }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + } else { + assert.strictEqual(converters.Uint8Array(view, { allowShared: true }), view); + } + assert.strictEqual(converters.Uint8Array(view, { + allowShared: true, + allowResizable: true, + }), view); + } + } +}); + +test('Shared WebAssembly buffer growability is checked per buffer', { + skip: typeof WebAssembly === 'undefined', +}, () => { + const memory = new WebAssembly.Memory({ initial: 1, maximum: 2, shared: true }); + for (const [buffer, growable] of [ + [memory.buffer, false], + [memory.toResizableBuffer(), true], + [memory.toFixedLengthBuffer(), false], + ]) { + for (const value of [buffer, new Uint8Array(buffer), new DataView(buffer)]) { + if (growable) { + assert.throws(() => converters.AllowSharedBufferSource(value), { + code: 'ERR_INVALID_ARG_TYPE', + }); + } else { + assert.strictEqual(converters.AllowSharedBufferSource(value), value); + } + assert.strictEqual(converters.AllowSharedBufferSource(value, { + allowResizable: true, + }), value); + } + const view = new Uint8Array(buffer); + if (growable) { + assert.throws(() => converters.Uint8Array(view, { allowShared: true }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + } else { + assert.strictEqual(converters.Uint8Array(view, { allowShared: true }), view); + } + } +}); + test('BufferSource rejects objects with a forged @@toStringTag', () => { const fake = { [Symbol.toStringTag]: 'Uint8Array' }; assert.throws( diff --git a/typings/internalBinding/util.d.ts b/typings/internalBinding/util.d.ts index a3026b5a0305..4c3eeca8ed82 100644 --- a/typings/internalBinding/util.d.ts +++ b/typings/internalBinding/util.d.ts @@ -47,6 +47,7 @@ export interface UtilBinding { styleText(format: Array | string, text: string): string; isInsideNodeModules(frameLimit?: number): boolean; constructSharedArrayBuffer(length?: number): SharedArrayBuffer; + getSharedArrayBufferGrowable(this: SharedArrayBuffer): boolean; constants: { kPending: 0; From 839498a02ee733caf47c1c23f4c62cfe85a2d40c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 10 Sep 2026 20:46:24 +0200 Subject: [PATCH 05/22] lib: avoid repeat internal receiver checks Let internal helpers access the private cache directly to avoid repeating receiver checks. Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/65910 Refs: https://github.com/nodejs/node/pull/65846 Reviewed-By: Xuguang Mei Reviewed-By: Yagiz Nizipli --- lib/internal/crypto/keys.js | 19 +++- .../test-webcrypto-cryptokey-brand-check.js | 104 +++++++++--------- ...test-webcrypto-cryptokey-clone-transfer.js | 12 ++ 3 files changed, 78 insertions(+), 57 deletions(-) diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 5f8bf3b603a0..b92adb28231e 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -1167,6 +1167,9 @@ const { if (depth < 0) return this; + if (!isCryptoKey(this)) + throw new ERR_INVALID_THIS('CryptoKey'); + const opts = { ...options, depth: options.depth == null ? null : options.depth - 1, @@ -1181,14 +1184,20 @@ const { } get type() { + if (!isCryptoKey(this)) + throw new ERR_INVALID_THIS('CryptoKey'); return getCryptoKeyType(this); } get extractable() { + if (!isCryptoKey(this)) + throw new ERR_INVALID_THIS('CryptoKey'); return getCryptoKeyExtractable(this); } get algorithm() { + if (!isCryptoKey(this)) + throw new ERR_INVALID_THIS('CryptoKey'); const slots = getSlots(this); let cached = slots[kSlotClonedAlgorithm]; if (cached === undefined) { @@ -1199,6 +1208,8 @@ const { } get usages() { + if (!isCryptoKey(this)) + throw new ERR_INVALID_THIS('CryptoKey'); const slots = getSlots(this); let cached = slots[kSlotClonedUsages]; if (cached === undefined) { @@ -1233,12 +1244,8 @@ const { return #slots in key || isNativeCryptoKey(key); }; getSlots = (key) => { - if (!key || typeof key !== 'object') - throw new ERR_INVALID_THIS('CryptoKey'); - if (#slots in key) { - const cached = key.#slots; - if (cached !== undefined) return cached; - } + const cached = key.#slots; + if (cached !== undefined) return cached; const slots = nativeGetCryptoKeySlots(key); slots[kSlotAlgorithm] = cloneInternalAlgorithm(slots[kSlotAlgorithm]); key.#slots = slots; diff --git a/test/parallel/test-webcrypto-cryptokey-brand-check.js b/test/parallel/test-webcrypto-cryptokey-brand-check.js index 9dd115f00721..9174aebe8f05 100644 --- a/test/parallel/test-webcrypto-cryptokey-brand-check.js +++ b/test/parallel/test-webcrypto-cryptokey-brand-check.js @@ -1,13 +1,10 @@ 'use strict'; -// The four CryptoKey prototype getters (`type`, `extractable`, -// `algorithm`, `usages`) are user-configurable per Web IDL, so they -// can be invoked with an arbitrary `this`. The native callbacks that -// implement them must brand-check their receiver and throw cleanly -// (ERR_INVALID_THIS) rather than crashing the process or returning -// garbage. This test exercises four progressively more hostile -// receiver shapes, including subverting `instanceof` via -// `Symbol.hasInstance`, to make sure the C++ brand check holds. +// CryptoKey prototype getters and methods can be invoked with an +// arbitrary `this`. They must brand-check their receiver and throw +// cleanly (ERR_INVALID_THIS) rather than crashing the process or +// returning garbage. This test exercises invalid receiver shapes, +// including subverting `instanceof` via `Symbol.hasInstance`. // // It also verifies that `util.types.isCryptoKey()` cannot be fooled // by prototype spoofing. @@ -17,7 +14,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('node:assert'); -const { types: { isCryptoKey } } = require('node:util'); +const { inspect, types: { isCryptoKey } } = require('node:util'); const { subtle } = globalThis.crypto; (async () => { @@ -29,22 +26,16 @@ const { subtle } = globalThis.crypto; const CryptoKey = key.constructor; - // Capture the underlying prototype getters once, so that subsequent + // Capture the underlying prototype members once, so that subsequent // tampering with `CryptoKey.prototype` cannot affect what we call. - const getters = { - type: Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'type').get, - extractable: - Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'extractable').get, - algorithm: - Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'algorithm').get, - usages: - Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'usages').get, - }; + const descriptors = Object.getOwnPropertyDescriptors(CryptoKey.prototype); // Sanity: each getter works on a real CryptoKey. - Object.entries(getters).forEach(([name, getter]) => { - assert.notStrictEqual(getter.call(key), undefined, `baseline ${name}`); - }); + for (const name of Reflect.ownKeys(descriptors)) { + const { get } = descriptors[name]; + if (get !== undefined) + Reflect.apply(get, key, []); + } assert.strictEqual(isCryptoKey(key), true); assert.strictEqual(Object.hasOwn(CryptoKey, 'getSlots'), false); const internalProto = Object.getPrototypeOf(key); @@ -56,36 +47,51 @@ const { subtle } = globalThis.crypto; const invalidThis = { code: 'ERR_INVALID_THIS', name: 'TypeError' }; const invalidArgType = { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' }; + async function assertInvalidReceiver(receiver) { + for (const name of Reflect.ownKeys(descriptors)) { + if (name === 'constructor') continue; + const descriptor = descriptors[name]; + const args = name === inspect.custom ? [0, {}] : []; + for (const kind of ['get', 'set', 'value']) { + const member = descriptor[kind]; + if (typeof member !== 'function') continue; + await assert.rejects( + async () => Reflect.apply(member, receiver, args), + invalidThis, + `CryptoKey.${String(name)} (${kind})`, + ); + } + } + } + // Plain object receiver. - Object.entries(getters).forEach(([, getter]) => { - assert.throws(() => getter.call({}), invalidThis); - }); + await assertInvalidReceiver({}); // Null-prototype object receiver. - Object.entries(getters).forEach(([, getter]) => { - assert.throws(() => getter.call({ __proto__: null }), invalidThis); - }); + await assertInvalidReceiver({ __proto__: null }); // Primitive receiver. - Object.entries(getters).forEach(([, getter]) => { - assert.throws(() => getter.call(1), invalidThis); - }); + await assertInvalidReceiver(1); // Null. - Object.entries(getters).forEach(([, getter]) => { - // eslint-disable-next-line no-useless-call - assert.throws(() => getter.call(null), invalidThis); - }); + await assertInvalidReceiver(null); // Undefined. - Object.entries(getters).forEach(([, getter]) => { - assert.throws(() => getter.call(), invalidThis); - }); + await assertInvalidReceiver(undefined); // Function - Object.entries(getters).forEach(([, getter]) => { - assert.throws(() => getter.call(function() {}), invalidThis); - }); + await assertInvalidReceiver(function() {}); + + const revoked = Proxy.revocable(key, {}); + revoked.revoke(); + for (const receiver of [ + { __proto__: CryptoKey.prototype }, + { __proto__: key }, + new Proxy(key, {}), + revoked.proxy, + ]) { + await assertInvalidReceiver(receiver); + } // Prototype spoofing with InternalCryptoKey.prototype must not pass // util.types.isCryptoKey(). @@ -111,9 +117,7 @@ const { subtle } = globalThis.crypto; const fake = { foo: 'bar' }; assert.strictEqual(fake instanceof CryptoKey, true); assert.strictEqual(isCryptoKey(fake), false); - Object.entries(getters).forEach(([, getter]) => { - assert.throws(() => getter.call(fake), invalidThis); - }); + await assertInvalidReceiver(fake); // Subverted `instanceof` plus a real BaseObject of a different // kind (a Buffer) as the receiver. Without the C++ tag check @@ -121,13 +125,11 @@ const { subtle } = globalThis.crypto; const buf = Buffer.alloc(16); assert.strictEqual(buf instanceof CryptoKey, true); assert.strictEqual(isCryptoKey(buf), false); - Object.entries(getters).forEach(([, getter]) => { - assert.throws(() => getter.call(buf), invalidThis); - }); + await assertInvalidReceiver(buf); // The real CryptoKey continues to work after all of the above. - assert.strictEqual(getters.type.call(key), 'secret'); - assert.strictEqual(getters.extractable.call(key), true); - assert.strictEqual(getters.algorithm.call(key).name, 'HMAC'); - assert.deepStrictEqual(getters.usages.call(key), ['sign']); + assert.strictEqual(descriptors.type.get.call(key), 'secret'); + assert.strictEqual(descriptors.extractable.get.call(key), true); + assert.strictEqual(descriptors.algorithm.get.call(key).name, 'HMAC'); + assert.deepStrictEqual(descriptors.usages.get.call(key), ['sign']); })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-cryptokey-clone-transfer.js b/test/parallel/test-webcrypto-cryptokey-clone-transfer.js index 4983e1c0bda3..2567ac69454d 100644 --- a/test/parallel/test-webcrypto-cryptokey-clone-transfer.js +++ b/test/parallel/test-webcrypto-cryptokey-clone-transfer.js @@ -17,6 +17,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('node:assert'); +const { KeyObject } = require('node:crypto'); const { inspect } = require('node:util'); const { once } = require('node:events'); const { Worker, MessageChannel } = require('node:worker_threads'); @@ -320,6 +321,17 @@ async function checkRsaPssTransferToWorker({ publicKey, privateKey }) { { name: 'AES-GCM', iv }, k, ciphertext); assert.deepStrictEqual(Buffer.from(decrypted), plaintext); } + + const bytes = new Uint8Array(await subtle.exportKey('raw', key)); + const nullPrototypeClone = structuredClone(key); + Object.setPrototypeOf(nullPrototypeClone, null); + assert.deepStrictEqual( + new Uint8Array(await subtle.exportKey('raw', nullPrototypeClone)), bytes); + const typeGetter = Object.getOwnPropertyDescriptor(key.constructor.prototype, 'type').get; + const customInspect = key[inspect.custom]; + assert.strictEqual(typeGetter.call(nullPrototypeClone), 'secret'); + assert.strictEqual(typeof customInspect.call(nullPrototypeClone, 0, {}), 'string'); + assert.deepStrictEqual(KeyObject.from(structuredClone(key)).export(), Buffer.from(bytes)); } // ECDSA keypair (public extractable, private non-extractable) From 80798e3db10b84ec198473b97547200e4f16a522 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 7 Sep 2026 23:42:13 +0200 Subject: [PATCH 06/22] crypto: optimize private EC JWK import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoid creating temporary EVP objects and repeating key validation while retaining the private scalar range and public/private consistency checks. Assisted-by: GitHub Copilot Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/65908 Reviewed-By: Yagiz Nizipli Reviewed-By: James M Snell Reviewed-By: Tobias Nießen --- deps/ncrypto/ncrypto.cc | 18 +++++ deps/ncrypto/ncrypto.h | 1 + src/crypto/crypto_ec.cc | 10 ++- .../test-crypto-key-objects-ec-jwk-private.js | 79 +++++++++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-crypto-key-objects-ec-jwk-private.js diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 99aa927a1f06..12c9d2180eda 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -5317,6 +5317,24 @@ bool ECPointPointer::mul(const EC_GROUP* group, const BIGNUM* priv_key) { // ============================================================================ +bool ECKeyPointer::checkPrivateKey() const { + const auto group = getGroup(); + const auto priv = getPrivateKey(); + const auto pub = getPublicKey(); + if (group == nullptr || priv == nullptr || pub == nullptr) return false; + + auto order = BignumPointer::New(); + if (!order || !EC_GROUP_get_order(group, order.get(), nullptr) || + BN_is_zero(priv) || BN_is_negative(priv) || + BN_cmp(priv, order.get()) >= 0) { + return false; + } + + auto expected = ECPointPointer::New(group); + return expected && expected.mul(group, priv) && + EC_POINT_cmp(group, expected.get(), pub, nullptr) == 0; +} + #if NCRYPTO_USE_LEGACY_KEY_TYPES ECKeyPointer::ECKeyPointer() : key_(nullptr) {} diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index a8a8c7fc66d7..944c42490d6c 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -1773,6 +1773,7 @@ class ECKeyPointer final { bool setPublicKeyRaw(const BignumPointer& x, const BignumPointer& y); bool generate(); bool checkKey() const; + bool checkPrivateKey() const; DataPointer computeSecret(const ECPointPointer& peer) const; const EC_GROUP* getGroup() const; diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index 4cfadfe67cef..0dea1f597d00 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -870,11 +870,19 @@ KeyObjectData ImportJWKEcKey(Environment* env, Local jwk) { THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key"); return {}; } + // Verify that the public point matches the private scalar (d*G == (x,y)). + if (!ec.checkPrivateKey()) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key"); + return {}; + } } auto pkey = EVPKeyPointer::New(); if (!pkey) return {}; - CHECK(pkey.set(ec)); + if (!pkey.set(ec)) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key"); + return {}; + } return KeyObjectData::CreateAsymmetric(type, std::move(pkey)); } diff --git a/test/parallel/test-crypto-key-objects-ec-jwk-private.js b/test/parallel/test-crypto-key-objects-ec-jwk-private.js new file mode 100644 index 000000000000..bede4e2f31c0 --- /dev/null +++ b/test/parallel/test-crypto-key-objects-ec-jwk-private.js @@ -0,0 +1,79 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + createECDH, + createPrivateKey, + createPublicKey, + getCurves, + getFips, + sign, + verify, +} = require('crypto'); + +const curves = [ + ['prime256v1', 'P-256', 32, + 'ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551'], + ['secp384r1', 'P-384', 48, + 'ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf' + + '581a0db248b0a77aecec196accc52973'], + ['secp521r1', 'P-521', 66, + '01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + + 'fa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409'], +]; +if (!getFips() && getCurves().includes('secp256k1')) { + curves.push(['secp256k1', 'secp256k1', 32, + 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141']); +} + +for (const [namedCurve, crv, width, orderHex] of curves) { + const order = BigInt(`0x${orderHex}`); + const encode = (scalar) => Buffer.from( + scalar.toString(16).padStart(width * 2, '0'), 'hex'); + const makeJwk = (scalar) => { + const ecdh = createECDH(namedCurve); + ecdh.setPrivateKey(encode(scalar)); + const point = ecdh.getPublicKey(); + return { + kty: 'EC', + crv, + x: point.subarray(1, 1 + width).toString('base64url'), + y: point.subarray(1 + width).toString('base64url'), + d: encode(scalar).toString('base64url'), + }; + }; + const generator = makeJwk(1n); + const other = makeJwk(2n); + const message = Buffer.from('EC JWK private key consistency'); + + for (const jwk of [generator, other, makeJwk(order - 1n)]) { + const key = createPrivateKey({ format: 'jwk', key: jwk }); + assert.deepStrictEqual(key.export({ format: 'jwk' }), jwk); + const publicJwk = { kty: jwk.kty, crv, x: jwk.x, y: jwk.y }; + const publicKey = createPublicKey({ format: 'jwk', key: publicJwk }); + assert(verify('sha256', message, publicKey, sign('sha256', message, key))); + } + + const invalid = [ + { ...generator, d: other.d }, + { ...generator, x: other.x, y: other.y }, + ...[0n, order, order + 1n].map((scalar) => ({ + ...generator, d: encode(scalar).toString('base64url'), + })), + { ...generator, d: '' }, + { + ...generator, + x: Buffer.alloc(width).toString('base64url'), + y: Buffer.alloc(width).toString('base64url'), + }, + ]; + for (const jwk of invalid) { + assert.throws(() => createPrivateKey({ format: 'jwk', key: jwk }), { + code: 'ERR_CRYPTO_INVALID_JWK', + }); + } +} From 968f6e1026dff0625b7d15284bb8402f55555115 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 11 Sep 2026 13:15:01 +0200 Subject: [PATCH 07/22] test: use named parameters in DH stress test Named modp14 parameters avoid generating and repeatedly validating a custom prime. Keep the existing exchange counts and FIPS rejection assertion. The separate deterministic padding test continues to cover imported prime parameters. Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/65980 Reviewed-By: James M Snell Reviewed-By: LiviaMedeiros --- test/pummel/test-dh-regr.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/test/pummel/test-dh-regr.js b/test/pummel/test-dh-regr.js index c442fbc3a809..8a2e71745a38 100644 --- a/test/pummel/test-dh-regr.js +++ b/test/pummel/test-dh-regr.js @@ -34,7 +34,6 @@ const assert = require('assert'); const crypto = require('crypto'); const { hasOpenSSL, hasFIPS } = require('../common/crypto'); -let p; let iterations = 2000; if (hasFIPS(3)) { assert.throws(() => crypto.createDiffieHellman(1024), { @@ -42,22 +41,24 @@ if (hasFIPS(3)) { name: 'TypeError', }); - // Use a precomputed approved group instead of generating a 2048-bit prime - // for every test run. Its larger keys also make each pummel iteration more - // expensive, so use enough iterations to exercise the regression without - // making the FIPS job excessively slow. - p = crypto.getDiffieHellman('modp14').getPrime(); + // Keep a lower iteration count for FIPS jobs. iterations = 100; +} + +let createDH; +if (hasOpenSSL(3)) { + // OpenSSL 3 recognizes named groups without validating their primes. + createDH = () => crypto.getDiffieHellman('modp14'); } else { - // FIPS requires length >= 1024, but small parameters keep this pummel test - // from timing out in ordinary CI. - const length = crypto.getFips() === 1 ? 1024 : (hasOpenSSL(3) ? 512 : 256); - p = crypto.createDiffieHellman(length).getPrime(); + // Other backends validate each peer's parameters, so keep them small. + const length = crypto.getFips() === 1 ? 1024 : 256; + const prime = crypto.createDiffieHellman(length).getPrime(); + createDH = () => crypto.createDiffieHellman(prime); } for (let i = 0; i < iterations; i++) { - const a = crypto.createDiffieHellman(p); - const b = crypto.createDiffieHellman(p); + const a = createDH(); + const b = createDH(); a.generateKeys(); b.generateKeys(); From 536c80be5572ce9f287ba8db48bbb0cac39673c0 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 11 Sep 2026 13:32:54 +0200 Subject: [PATCH 08/22] test: reuse fixed primes in DH tests Use the modp14 prime instead of generating fresh parameters for tests of constructors, key setters, and memory retention. Keep generic DiffieHellman instances and the existing setter and leak assertions. Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/65980 Reviewed-By: James M Snell Reviewed-By: LiviaMedeiros --- test/parallel/test-crypto-dh-constructor.js | 9 +++------ test/parallel/test-crypto-dh-generate-keys.js | 6 ++---- test/parallel/test-crypto-dh-leak.js | 6 ++---- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/test/parallel/test-crypto-dh-constructor.js b/test/parallel/test-crypto-dh-constructor.js index 28747ac3a726..edf7ab08e44c 100644 --- a/test/parallel/test-crypto-dh-constructor.js +++ b/test/parallel/test-crypto-dh-constructor.js @@ -5,17 +5,14 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL, hasFIPS } = require('../common/crypto'); +const { hasFIPS } = require('../common/crypto'); -const size = hasFIPS(3) ? - 2048 : (crypto.getFips() === 1 || hasOpenSSL(3) ? 1024 : 256); -const dh1 = crypto.createDiffieHellman(size); -const p1 = dh1.getPrime('buffer'); +const prime = crypto.getDiffieHellman('modp14').getPrime('buffer'); { const DiffieHellman = crypto.DiffieHellman; - const dh = DiffieHellman(p1, 'buffer'); + const dh = DiffieHellman(prime, 'buffer'); assert(dh instanceof DiffieHellman, 'DiffieHellman is expected to return a ' + 'new instance when called without `new`'); } diff --git a/test/parallel/test-crypto-dh-generate-keys.js b/test/parallel/test-crypto-dh-generate-keys.js index d074ba957516..65efb369c08e 100644 --- a/test/parallel/test-crypto-dh-generate-keys.js +++ b/test/parallel/test-crypto-dh-generate-keys.js @@ -6,11 +6,9 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL, hasFIPS } = require('../common/crypto'); { - const size = hasFIPS(3) ? - 2048 : (crypto.getFips() === 1 || hasOpenSSL(3) ? 1024 : 256); + const prime = crypto.getDiffieHellman('modp14').getPrime(); function unlessInvalidState(f) { try { @@ -23,7 +21,7 @@ const { hasOpenSSL, hasFIPS } = require('../common/crypto'); } function testGenerateKeysChangesKeys(setup, expected) { - const dh = crypto.createDiffieHellman(size); + const dh = crypto.createDiffieHellman(prime); setup(dh); const firstPublicKey = unlessInvalidState(() => dh.getPublicKey()); const firstPrivateKey = unlessInvalidState(() => dh.getPrivateKey()); diff --git a/test/parallel/test-crypto-dh-leak.js b/test/parallel/test-crypto-dh-leak.js index 8d5141eef4b1..f58a5a152b78 100644 --- a/test/parallel/test-crypto-dh-leak.js +++ b/test/parallel/test-crypto-dh-leak.js @@ -9,13 +9,11 @@ if (common.isASan) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL, hasFIPS } = require('../common/crypto'); const before = process.memoryUsage.rss(); { - const size = hasFIPS(3) ? - 2048 : (crypto.getFips() === 1 || hasOpenSSL(3) ? 1024 : 256); - const dh = crypto.createDiffieHellman(size); + const prime = crypto.getDiffieHellman('modp14').getPrime(); + const dh = crypto.createDiffieHellman(prime); const publicKey = dh.generateKeys(); const privateKey = dh.getPrivateKey(); for (let i = 0; i < 5e4; i += 1) { From 8680c7a2e1dc233aa43c1cac0da9692f12b19e48 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 14 Sep 2026 13:44:13 +0200 Subject: [PATCH 09/22] test: fix RSA/DSA wrong-passphrase flake Re-encrypting keys on each FIPS test run can produce ciphertext that decrypts with valid padding under the wrong password, causing a decoder error instead of the expected bad decrypt. Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/65983 Refs: https://github.com/nodejs/node/actions/runs/34595689099/job/103251404626?pr=65980 Reviewed-By: James M Snell Reviewed-By: Matteo Collina --- test/fixtures/keys/Makefile | 6 +- test/fixtures/keys/dsa_private_encrypted.pem | 41 ++++++------- test/fixtures/keys/rsa_private_encrypted.pem | 60 ++++++++++---------- test/parallel/test-crypto-rsa-dsa.js | 31 ++-------- test/parallel/test-tls-passphrase.js | 45 ++++----------- 5 files changed, 67 insertions(+), 116 deletions(-) diff --git a/test/fixtures/keys/Makefile b/test/fixtures/keys/Makefile index 0960e69f36fd..bad4b6aed3ca 100644 --- a/test/fixtures/keys/Makefile +++ b/test/fixtures/keys/Makefile @@ -844,7 +844,8 @@ dsa_private.pem: dsa_params.pem openssl gendsa -out dsa_private.pem dsa_params.pem dsa_private_encrypted.pem: dsa_private.pem - openssl dsa -aes256 -in dsa_private.pem -passout 'pass:password' -out dsa_private_encrypted.pem + openssl pkcs8 -topk8 -v2 aes-256-cbc -v2prf hmacWithSHA256 -iter 2048 -saltlen 16 \ + -in $< -passout 'pass:password' -out $@ dsa_private_pkcs8.pem: dsa_private.pem openssl pkcs8 -topk8 -inform PEM -outform PEM -in dsa_private.pem -out dsa_private_pkcs8.pem -nocrypt @@ -868,7 +869,8 @@ rsa_private.pem: openssl genrsa -out rsa_private.pem 2048 rsa_private_encrypted.pem: rsa_private.pem - openssl rsa -aes256 -in rsa_private.pem -passout 'pass:password' -out rsa_private_encrypted.pem + openssl pkcs8 -topk8 -v2 aes-256-cbc -v2prf hmacWithSHA256 -iter 2048 -saltlen 16 \ + -in $< -passout 'pass:password' -out $@ rsa_private_pkcs8.pem: rsa_private.pem openssl pkcs8 -topk8 -inform PEM -outform PEM -in rsa_private.pem -out rsa_private_pkcs8.pem -nocrypt diff --git a/test/fixtures/keys/dsa_private_encrypted.pem b/test/fixtures/keys/dsa_private_encrypted.pem index 49b3375baf71..88da77e0a3b7 100644 --- a/test/fixtures/keys/dsa_private_encrypted.pem +++ b/test/fixtures/keys/dsa_private_encrypted.pem @@ -1,23 +1,18 @@ ------BEGIN DSA PRIVATE KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: AES-256-CBC,FABA263DD471F214EF3E02699B837C20 - -Tj2+4x9MEIaQGFQ4o7hk12MriVYyvLO5aCbqq7LG5uhVk546/+bJc6hewdSwb6oT -MYPbuV+QTdtqshqFESA0McyGlj4w1tOg5TomP84NTKvwTO1EirVLMukfF3dqaguw -C117AZJkGbqgbi6lZ2bG0Hta6HRbhI5+ODFtOp3rKQ2KwVmtL7zw6vt3PCISeMHN -fLqikDc2+YoI9V1FJis9/FATyqV8yrJYYQQpP1RQN+gDY4SSs/eUr+Me7RNy6Lz7 -oH0tDaPGbiafwrZe1okksjxT2JQz1Q3hciBPikgdQIoE2NWTUlOeRYX0T0N2n37S -6Odbcr522e+2XjcLj34Ozthp+Q5mIDcLuakazxkXhq0RyhJ7vo+xA2YiP7Q3vH7g -oAnsJPFNVY6wJhprZi2VofKIUJUiajAXGDVX2yEIG/DOA9rnx0ZP+zopXMi4ptu0 -RzWyAL+P4jn0b8vgPf9CYJmn4VNfOcVmomZ1Bw6hzqTE2FnThJCXU3l2eaC/wcSR -uMRp8c6IM8AR5DUzUBKIckkvXj1m5iSZoKuR8dB7s9BhrRtBAI7K3G254G06sByv -0pnft8r+BkMqgdfG4rJQoQJw7tVYln+pL/gYPDuYsqyJ9kFuHDqtBvlozqXY5AL1 -XQaXoD6xMACEoJSIv5y+TzFzXwFQrDW+G1724YOSbiioUfGD0tRfjj2ei63PThQr -Z50SryfKQQf4UgcJeokMhmRWT2vPXEFWEP1b2FMEQxBy6fyKcqwZBAbhqF6usGEB -nwr/S1HXQAGEsWoc/Z4yynB7uhOwWu/Vpj+V6B98NmC7EUX15Why8zCsT9gzaIgC -M6sZafHhcmjfwc+lL9xFlU/wnAOz0LeKZWry3D0sXZn1r2FRlOJdtLLx01Sve/MU -ZRsgEDTkzv8E9dDltMeq8HQDCgLT1USTMWcY1kMELBj7y7ZdCWjH1QhTq2KlId+o -1X28zJOsOL/XRseUSlpjmSSLRw1QQEypNCY2+tcvViAvn3AifipBbdzUNhvygLhc -a2+5rYsd8BBEFnMJx7lDiyqXGnZkBbhbCSIudppNcjC+akFlFp6fBzkp4mKBuKpc -hwBBdfqdEyzqu6SVHM8nGV/aDoRuu9shV6MX0y/KnIgLedudn8aN2eLgjR5k1+99 ------END DSA PRIVATE KEY----- +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIC1TBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQUHwLuG5KIFDnU6OC +5ADEzQICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEOrzdylfA3E36eSA +aLsD6C0EggJwrFQCTATMWun5ffHFf42epvxuRH3Dv96gOejJxmItv3fqF7cfn+oB +82gsGwdcjme6eGu7rg4lKi5e85WEImcg+xQdHnEmx62yP73CBfpzCs5KObbBd8ft +EjlqQ+EWQ68+acdHOZ/xDqBiJnkisEVEnsTo9ZE7ZLVBKRP6Hr31eQ/7EKWBb6Ax +oF+6+WgvyH8AuOO17CLjMOGO9pFl2TvJ1wsQtAIb2ZEAmPp0Z/lT9qXaoJfAz4pO +o3+2woDPDm9PA6WXF8zD+zPBwnSBSPjvkwtsMp1CdhrAxaeEo+gbDqJjCI9/2m8j +gAqkP+l7civZ7P7ydOPmlkiSdl4TXfhvfhk4RCSQbg6+YozggJO0ZiroCK7G5ro6 +4T+t2ck206kylOZV1xPrbW+Kt88vIVSxNs8dwHplEtb1VdoAV4EKfh3PEcsuSc4W +NEGvaAEmzckAMG3GJgjZYS8EWfpLsuNRcvRJsbMeQCC4Iex9m4+KJVUmMmIBlBxV +aoFEfYNGpkOdMV7l1Ne77c4HGI7NS6Yy+JcxYoc5kByTxaWOSvPSoQ3YQ+bY3GoT +tAQRvoieBnzkkyjKaS0Qgnfkdfe4Ry7hSjToo3mOnA8wj6sjRtZdIQihTa1jZ3vP +ELJ8lPTVBiPUC/AZ0C12s/JExY3y598Rlt7RQRA6ZN6PP9gnuuqR58sq/XoLnwBQ +tHK8kz5BU4EZ2neMi7noHDmArgh083WGFX6WUAfDu9R8+hydnee86Wm4xThXk5NS +TIGqwBal/5EgrSCbkcwxRw9xB0afMD4+muatKv6NEcRKT/VjhxdWP4gHONrh3tJF +/pn2+4MtHeK8 +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/fixtures/keys/rsa_private_encrypted.pem b/test/fixtures/keys/rsa_private_encrypted.pem index f1914289ec4f..6a605a055551 100644 --- a/test/fixtures/keys/rsa_private_encrypted.pem +++ b/test/fixtures/keys/rsa_private_encrypted.pem @@ -1,30 +1,30 @@ ------BEGIN RSA PRIVATE KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: AES-256-CBC,DB3D20E60E8FDC3356BD79712FF8EF7E - -K+vu0U3IFTJBBi6zW5Zng80O1jXq/ZmlOFs/j/SQpPwfW1Do9i/Dwa7ntBlTwrCm -sd3IIPgu2ikfLwxvbxsZN540oCaCqaZ/bmmyzH3MyVDA9MllUu+X8+Q3ATzcYa9R -U5XfF5DAXsSRnstCbmKagWVQpO0oX8k3ratfny6Ixq86Y82tK8+o5YiBFq1kqa+9 -4yat7IWQbqV5ifUtUPCHZwEqBt+WKazX05BqERjkckHdpfaDrBvSSPXTwoLm6uRR -ktkUVpO4tHMZ4VlcTfFtpz8gdYYod0nM6vz26hvbESHSwztSgMhmKdsE5eqmYfgu -F4WkEN4bqAiPjKK3jnUKPt/vg2oKYFQlVYFl9QnBjiRqcQTi3e9lwn1hI7uoMb6g -HuaCc57JJHPN/ZLP3ts4ZxFbwUjTGioh5Zh6WozG3L3+Ujwq/sDrAskRyzdcuP7I -Rs3oLbHY03OHyg8IbxR5Iu89l6FLqnR45yvbxXtZ7ImGOPM5Z9pB1CzDhGDx2F6g -J/Kf/7ZF2DmYUVbVKDfESEDhRfuMAVzhasDPTRqipSA5QvJVQY+J/6QDPrNNmHVB -4e4ouHIDWERUf0t1Be7THvP3X8OJozj2HApzqa5ZCaJDo8eaL8TCD5uH75ID5URJ -VscGHaUXT8/sxfHi1x8BibW5W5J/akFsnrnJU/1BZgGznIxjf5tKfHGppSIVdlKP -3ghYNmEIFPNJ6cxuUA0D2IOV4uO3FTCU6seIzvJhYkmXnticcZYGtmGxXKrodtzS -J1YuaNkkO/YRZah285lQ6QCIhCFo4Oa4ILjgoTQISuw7nQj5ESyncauzLUBXKX0c -XDUej64KNTvVF9UXdG48fYvNmSZWCnTye4UmPu17FmwpVra38U+EdoLyWyMIAI5t -rP6Hhgc9BxOo41Im9QpTcAPfKAknP8Rbm3ACJG5T9FKq/c29d1E//eFR6SL51e/a -yWdCgJN/FJOAX60+erPwoVoRFEttAeDPkklgFGdc8F4LIYAig9gEZ92ykFFz3fWz -jIcUVLrL+IokFbPVUBoMihqVyMQsWH+5Qq9wjxf6EDIf0BVtm9U4BJoOkPStFIfF -Kof7OVv7izyL8R/GIil9VQs9ftwkIUPeXx2Hw0bE3HJ3C8K4+mbLg3tKhGnBDU5Z -Xm5mLHoCRBa3ZRFWZtigX7POszdLAzftYo8o65Be4OtPS+tQAORk9gHsXATv7dDB -OGw61x5KA55LHVHhWaRvu3J8E7nhxw0q/HskyZhDC+Y+Xs6vmQSb4nO4ET4NYX1P -m3PMdgGoqRDJ2jZw4eoQdRKCM0EHSepSAYpO1tcAXhPZS4ITogoRgPpVgOebEQUL -nKNeNu/BxMSH/IH15jjDLF3TiEoguF9xdTaCxIBzE1SFpVO0u9m9vXpWdPThVgsb -VcEI487p7v9iImP3BYPT8ZYvytC26EH0hyOrwhahTvTb4vXghkLIyvPUg1lZHc6e -aPHb2AzYAHLnp/ehDQGKWrCOJ1JE2vBv8ZkLa+XZo7YASXBRZitPOMlvykEyzxmR -QAmNhKGvFmeM2mmHAp0aC03rgF3lxNsXQ1CyfEdq3UV9ReSnttq8gtrJfCwxV+wY ------END RSA PRIVATE KEY----- +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQ2cq1Ok3DMP2A/RoK +n3/9ZgICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEGRKDlxB0C8WgbEc +enhkuocEggTQQK9exbwIpgV5QRoHDMACpXuWtsZgdpCanhtA53pP8tELYc4MzbIp +90dLQWxqi/mSlqr1ifoMnCFEAKVUV8mDoh0i5uvQhxOC6TB0HBGgb7dEgITIC7NT +PTeHNepQyW97SkTMKoTnOogxgWeAHKYsSALHJw+45q4qARE7vo1O8CHE6v6Liv4F +ZaW/8p2O4qUTRWGshUG6tC4/YN6r/iYtoSXUzOTiTeLckhV/9QsP5pGF3LQXIU9I +5xh+I1BdcSZ/7sGiHED0BB4yF2lOUKKUwCNFUTddhApewYm/yCW8wTYB7vvOI8mz +47V8rfPj40Qzislrc5Sm96XpQp8DrLV4KKkA8F1MPEchHgOcdq+1UKCRW2kfUOWS +T9wSXPHlmuKxHsrXk3pBGdnshbp2d5d4uwVhi9bPA7J2DN25ayIe9H8LbYfPo58C +CiCnjLG02nKYSLjFZR8K/z2j+xmGyXVcHuGZzR02O3G4G89VlxxfazxYbZZvSNbM +w6/TjbSDmvOYLRxRRdbZRWixgyn4QRmg5QmWBbqsS1YmVOG3HafHdAtS2B++DRF3 +S5Waaa/mrqzMesl+TLYJ759gHOqODoppm1iwmVzcBuoDLYNUI0AZZ/pEGs3oGk4M +faNiVMcsFkMWPSzgTv/viWJgJeQdWIos75nyTUgDJiFmxMT6HcWZxEFBM5IMqoZJ +JXfFGIwPo5pjw8kFHh/4Mj3qCuQ1jxrnRFdC3EhSy+JCQf0S4XTYws4GBZ4psRTX +KOb+VqizDL+0A9d88QIQX3BWfCZ6QhFHd4JdLhZt1myjsLu50NaraHBb/9pEcBUb +/hIKTxQCgXZMw1XaJeXRtbObYeB1JjPZYfuaxwvYIQ1CrPVHcCEZYH/GlQ2ppqCf +RIZY6Tp522nQ7l7uOhhJozkXZsbI5/MzaYQguz+dRAWBckQTAgStRJ2UeCP215CN +cSY/nSeIssjov+Rs2twi/AFglET7f0Osg2YXjEl6zRde80D7/74a/YASw/qGP/+q +nxwVmMpcLCfvfvb7JN0YofU5TLbOYt3lneydIe8aZhR15HlamnnZS8cs+2FeiFu2 +I4tH+7FAiB/HuYSTJSKWvTbZX100aCDqsyYW0UF4mrNgcIdjjrmWAJL2thcX5JmW ++PTTbDKGtsnGm5QUqtYHQTWelHNeWXwxveAWReG3d7v5vx+ioY1xWKkEzrRjGtNJ +nmNvFBSjlz28fxhlJ6XV3DO2N3VvPwiXLYKE99idl2mtX0WWSCcnsYEKWRSccxe7 +quwxYxWa1l4t07OUCbMCsTz4weBMc/7DK/xUSKETp/C8IR6o/F8iK49KjpIQFnjQ +abJy1RSNxRRawqDdLqZiLjAZydBq7lJ4R3LTqEdw4zQGpFdGa/YAshkCPsIGCj3q +8ONcd4TQC4qd+tBhvJiyuuwvt+an5n9Axlo93NJweTTVDgR3FLsJL2FGV0FWa9o0 +aiUYdlL1fkAPOvVT97OLjmM/9RBirSUGR+Dwr6O3tRaB1bDmpKUqTz190Lhd3Ee/ +zWOJa6ubhUjls1KY4Rm/f28VheNRp6PQn+QdIdht/1oEqlssv/yPeVe1U6tW2oDW +ksbE0O1JvbUhKKhTD3cFY6x5A7iRflJpSEEoPqbHu8X237gEtgqJcKI= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/parallel/test-crypto-rsa-dsa.js b/test/parallel/test-crypto-rsa-dsa.js index bda98652cf59..dd38ecd4d68d 100644 --- a/test/parallel/test-crypto-rsa-dsa.js +++ b/test/parallel/test-crypto-rsa-dsa.js @@ -26,42 +26,19 @@ const keyPem = fixtures.readKey('rsa_private.pem'); const rsaKeySize = 2048; const rsaPubPem = fixtures.readKey('rsa_public.pem', 'ascii'); const rsaKeyPem = fixtures.readKey('rsa_private.pem', 'ascii'); -const rsaKeyPemEncryptedLegacy = fixtures.readKey( +// Fixed ciphertexts keep wrong passwords from occasionally producing valid +// padding and a decoder error instead of the expected bad decrypt. +const rsaKeyPemEncrypted = fixtures.readKey( 'rsa_private_encrypted.pem', 'ascii'); -const rsaKeyPemEncrypted = fips3 ? - crypto.createPrivateKey(rsaKeyPem).export({ - type: 'pkcs8', - format: 'pem', - cipher: 'aes-256-cbc', - passphrase: 'password', - }) : rsaKeyPemEncryptedLegacy; const dsaPubPem = fixtures.readKey('dsa_public.pem', 'ascii'); const dsaKeyPem = fixtures.readKey('dsa_private.pem', 'ascii'); -const dsaKeyPemEncryptedLegacy = fixtures.readKey( +const dsaKeyPemEncrypted = fixtures.readKey( 'dsa_private_encrypted.pem', 'ascii'); -const dsaKeyPemEncrypted = fips3 ? - crypto.createPrivateKey(dsaKeyPem).export({ - type: 'pkcs8', - format: 'pem', - cipher: 'aes-256-cbc', - passphrase: 'password', - }) : dsaKeyPemEncryptedLegacy; const rsaPkcs8KeyPem = fixtures.readKey('rsa_private_pkcs8.pem'); const dsaPkcs8KeyPem = fixtures.readKey('dsa_private_pkcs8.pem'); const ec = new TextEncoder(); -if (fips3) { - for (const key of [rsaKeyPemEncryptedLegacy, dsaKeyPemEncryptedLegacy]) { - assert.throws(() => crypto.createPrivateKey({ - key, - passphrase: 'password', - }), { - code: 'ERR_OSSL_EVP_UNSUPPORTED', - }); - } -} - const openssl1DecryptError = { message: 'error:06065064:digital envelope routines:EVP_DecryptFinal_ex:' + 'bad decrypt', diff --git a/test/parallel/test-tls-passphrase.js b/test/parallel/test-tls-passphrase.js index 1fe2c1ec11cf..6e1b50c0bb10 100644 --- a/test/parallel/test-tls-passphrase.js +++ b/test/parallel/test-tls-passphrase.js @@ -38,33 +38,6 @@ assert(Buffer.isBuffer(cert)); assert.strictEqual(typeof passKey.toString(), 'string'); assert.strictEqual(typeof cert.toString(), 'string'); -if (hasFIPS(3)) { - const encryptedKeyOptions = { - key: passKey, - passphrase: 'password', - cert, - }; - assert.throws(() => tls.Server(encryptedKeyOptions), { - code: 'ERR_OSSL_EVP_UNSUPPORTED', - }); - assert.throws(() => tls.connect(encryptedKeyOptions), { - code: 'ERR_OSSL_EVP_UNSUPPORTED', - }); - - const server = tls.Server({ key: rawKey, passphrase: 'ignored', cert }); - server.listen(0, common.mustCall(function() { - const client = tls.connect({ - port: this.address().port, - key: rawKey, - passphrase: 'ignored', - cert, - rejectUnauthorized: false, - }, common.mustCall(() => client.end())); - client.on('close', common.mustCall(() => server.close())); - })); - return; -} - function onSecureConnect() { return common.mustCall(function() { this.end(); }); } @@ -252,6 +225,10 @@ server.listen(0, common.mustCall(function() { })).unref(); const errMessageDecrypt = /bad[ _]decrypt/i; +// TLS supplies an empty password when the passphrase is omitted. OpenSSL 4 +// FIPS rejects it during PBKDF2 password-length checks, before decryption. +const missingPassphraseError = hasFIPS(4) ? + { code: 'ERR_OSSL_PASSWORD_STRENGTH_TOO_WEAK' } : errMessageDecrypt; // Missing passphrase assert.throws(function() { @@ -261,7 +238,7 @@ assert.throws(function() { cert: cert, rejectUnauthorized: false }); -}, errMessageDecrypt); +}, missingPassphraseError); assert.throws(function() { tls.connect({ @@ -270,7 +247,7 @@ assert.throws(function() { cert: cert, rejectUnauthorized: false }); -}, errMessageDecrypt); +}, missingPassphraseError); assert.throws(function() { tls.connect({ @@ -279,14 +256,14 @@ assert.throws(function() { cert: cert, rejectUnauthorized: false }); -}, errMessageDecrypt); +}, missingPassphraseError); // Invalid passphrase assert.throws(function() { tls.connect({ port: server.address().port, key: passKey, - passphrase: 'invalid', + passphrase: 'wrong-password', cert: cert, rejectUnauthorized: false }); @@ -296,7 +273,7 @@ assert.throws(function() { tls.connect({ port: server.address().port, key: [passKey], - passphrase: 'invalid', + passphrase: 'wrong-password', cert: cert, rejectUnauthorized: false }); @@ -306,7 +283,7 @@ assert.throws(function() { tls.connect({ port: server.address().port, key: [{ pem: passKey }], - passphrase: 'invalid', + passphrase: 'wrong-password', cert: cert, rejectUnauthorized: false }); @@ -315,7 +292,7 @@ assert.throws(function() { assert.throws(function() { tls.connect({ port: server.address().port, - key: [{ pem: passKey, passphrase: 'invalid' }], + key: [{ pem: passKey, passphrase: 'wrong-password' }], passphrase: 'password', // Valid but unused cert: cert, rejectUnauthorized: false From 0a757eed15bf223ec14868cd50da345a35bc17c1 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 5 Sep 2026 12:42:52 +0200 Subject: [PATCH 10/22] crypto: add Hybrid KEMs to Web Cryptography Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/65759 Reviewed-By: James M Snell Reviewed-By: Yagiz Nizipli --- doc/api/webcrypto.md | 50 +- lib/internal/crypto/kem_hybrids.js | 1173 +++++++++++++++++ lib/internal/crypto/keys.js | 67 +- lib/internal/crypto/util.js | 40 + lib/internal/crypto/webcrypto.js | 84 ++ lib/internal/util/comparisons.js | 18 +- src/crypto/README.md | 6 +- src/crypto/crypto_keys.cc | 99 +- src/crypto/crypto_keys.h | 38 +- src/crypto/crypto_util.h | 9 + .../webcrypto/supports-modern-algorithms.mjs | 26 + .../test-crypto-key-objects-to-crypto-key.js | 6 +- .../test-webcrypto-get-public-key.mjs | 8 + test/parallel/test-webcrypto-kem-hybrids.js | 689 ++++++++++ ...-webcrypto-promise-prototype-pollution.mjs | 13 + test/parallel/test-webcrypto-supports.mjs | 3 + typings/internalBinding/crypto.d.ts | 6 + 17 files changed, 2300 insertions(+), 35 deletions(-) create mode 100644 lib/internal/crypto/kem_hybrids.js create mode 100644 test/parallel/test-webcrypto-kem-hybrids.js diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index 2e54d76f0eae..f18f49bae1f7 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -2,6 +2,9 @@ + +* `bundle` {ArrayBuffer|Buffer|TypedArray|DataView} A DER-encoded PKCS#12 + (`.p12` or `.pfx`) bundle. +* `options` {Object} + * `passphrase` {string|ArrayBuffer|Buffer|TypedArray|DataView} The passphrase + protecting the bundle. Omitting this option is equivalent to passing `''`. +* Returns: {Object} + * `privateKey` {KeyObject|null} The first private key in the bundle, or + `null` if none is present. + * `certificate` {X509Certificate|null} The certificate matching `privateKey`, + or `null` if no matching certificate is present. + * `additionalCertificates` {X509Certificate\[]} All other certificates in + the bundle. If there is no private key, this contains all certificates. + May be empty. + +Parses a PKCS#12 bundle, commonly stored with a `.p12` or `.pfx` extension, +and returns its private key and certificates. + +```mjs +import { parsePKCS12 } from 'node:crypto'; +import { readFileSync } from 'node:fs'; + +const { privateKey, certificate, additionalCertificates } = parsePKCS12( + readFileSync('bundle.p12'), + { passphrase: 'secret' }, +); +``` + ### `crypto.pbkdf2(password, salt, iterations, keylen, digest, callback)`