@@ -11,11 +11,15 @@ const assert = require('node:assert')
1111const { isUint8Array } = require ( 'node:util/types' )
1212const { webidl } = require ( './webidl' )
1313
14+ let supportedHashes = [ ]
15+
1416// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
1517/** @type {import('crypto') } */
1618let crypto
1719try {
1820 crypto = require ( 'node:crypto' )
21+ const possibleRelevantHashes = [ 'sha256' , 'sha384' , 'sha512' ]
22+ supportedHashes = crypto . getHashes ( ) . filter ( ( hash ) => possibleRelevantHashes . includes ( hash ) )
1923/* c8 ignore next 3 */
2024} catch {
2125
@@ -565,66 +569,56 @@ function bytesMatch (bytes, metadataList) {
565569 return true
566570 }
567571
568- // 3. If parsedMetadata is the empty set, return true.
572+ // 3. If response is not eligible for integrity validation, return false.
573+ // TODO
574+
575+ // 4. If parsedMetadata is the empty set, return true.
569576 if ( parsedMetadata . length === 0 ) {
570577 return true
571578 }
572579
573- // 4 . Let metadata be the result of getting the strongest
580+ // 5 . Let metadata be the result of getting the strongest
574581 // metadata from parsedMetadata.
575- const list = parsedMetadata . sort ( ( c , d ) => d . algo . localeCompare ( c . algo ) )
576- // get the strongest algorithm
577- const strongest = list [ 0 ] . algo
578- // get all entries that use the strongest algorithm; ignore weaker
579- const metadata = list . filter ( ( item ) => item . algo === strongest )
582+ const strongest = getStrongestMetadata ( parsedMetadata )
583+ const metadata = filterMetadataListByAlgorithm ( parsedMetadata , strongest )
580584
581- // 5 . For each item in metadata:
585+ // 6 . For each item in metadata:
582586 for ( const item of metadata ) {
583587 // 1. Let algorithm be the alg component of item.
584588 const algorithm = item . algo
585589
586590 // 2. Let expectedValue be the val component of item.
587- let expectedValue = item . hash
591+ const expectedValue = item . hash
588592
589593 // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e
590594 // "be liberal with padding". This is annoying, and it's not even in the spec.
591595
592- if ( expectedValue . endsWith ( '==' ) ) {
593- expectedValue = expectedValue . slice ( 0 , - 2 )
594- }
595-
596596 // 3. Let actualValue be the result of applying algorithm to bytes.
597597 let actualValue = crypto . createHash ( algorithm ) . update ( bytes ) . digest ( 'base64' )
598598
599- if ( actualValue . endsWith ( '==' ) ) {
600- actualValue = actualValue . slice ( 0 , - 2 )
599+ if ( actualValue [ actualValue . length - 1 ] === '=' ) {
600+ if ( actualValue [ actualValue . length - 2 ] === '=' ) {
601+ actualValue = actualValue . slice ( 0 , - 2 )
602+ } else {
603+ actualValue = actualValue . slice ( 0 , - 1 )
604+ }
601605 }
602606
603607 // 4. If actualValue is a case-sensitive match for expectedValue,
604608 // return true.
605- if ( actualValue === expectedValue ) {
606- return true
607- }
608-
609- let actualBase64URL = crypto . createHash ( algorithm ) . update ( bytes ) . digest ( 'base64url' )
610-
611- if ( actualBase64URL . endsWith ( '==' ) ) {
612- actualBase64URL = actualBase64URL . slice ( 0 , - 2 )
613- }
614-
615- if ( actualBase64URL === expectedValue ) {
609+ if ( compareBase64Mixed ( actualValue , expectedValue ) ) {
616610 return true
617611 }
618612 }
619613
620- // 6 . Return false.
614+ // 7 . Return false.
621615 return false
622616}
623617
624618// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options
625619// https://www.w3.org/TR/CSP2/#source-list-syntax
626620// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
627- const parseHashWithOptions = / (?< algo > s h a 2 5 6 | s h a 3 8 4 | s h a 5 1 2 ) - (?< hash > [ A - Z a - z 0 - 9 + / ] + = { 0 , 2 } (? = \s | $ ) ) ( + [ ! - ~ ] * ) ? / i
621+ const parseHashWithOptions = / (?< algo > s h a 2 5 6 | s h a 3 8 4 | s h a 5 1 2 ) - ( ( ?<hash > [ A - Z a - z 0 - 9 + / ] + | [ A - Z a - z 0 - 9 _ - ] + ) = { 0 , 2 } (?: \s | $ ) ( + [ ! - ~ ] * ) ? ) ? / i
628622
629623/**
630624 * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
@@ -638,8 +632,6 @@ function parseMetadata (metadata) {
638632 // 2. Let empty be equal to true.
639633 let empty = true
640634
641- const supportedHashes = crypto . getHashes ( )
642-
643635 // 3. For each token returned by splitting metadata on spaces:
644636 for ( const token of metadata . split ( ' ' ) ) {
645637 // 1. Set empty to false.
@@ -649,7 +641,11 @@ function parseMetadata (metadata) {
649641 const parsedToken = parseHashWithOptions . exec ( token )
650642
651643 // 3. If token does not parse, continue to the next token.
652- if ( parsedToken === null || parsedToken . groups === undefined ) {
644+ if (
645+ parsedToken === null ||
646+ parsedToken . groups === undefined ||
647+ parsedToken . groups . algo === undefined
648+ ) {
653649 // Note: Chromium blocks the request at this point, but Firefox
654650 // gives a warning that an invalid integrity was given. The
655651 // correct behavior is to ignore these, and subsequently not
@@ -658,11 +654,11 @@ function parseMetadata (metadata) {
658654 }
659655
660656 // 4. Let algorithm be the hash-algo component of token.
661- const algorithm = parsedToken . groups . algo
657+ const algorithm = parsedToken . groups . algo . toLowerCase ( )
662658
663659 // 5. If algorithm is a hash function recognized by the user
664660 // agent, add the parsed token to result.
665- if ( supportedHashes . includes ( algorithm . toLowerCase ( ) ) ) {
661+ if ( supportedHashes . includes ( algorithm ) ) {
666662 result . push ( parsedToken . groups )
667663 }
668664 }
@@ -675,6 +671,82 @@ function parseMetadata (metadata) {
675671 return result
676672}
677673
674+ /**
675+ * @param {{ algo: 'sha256' | 'sha384' | 'sha512' }[] } metadataList
676+ */
677+ function getStrongestMetadata ( metadataList ) {
678+ // Let algorithm be the algo component of the first item in metadataList.
679+ // Can be sha256
680+ let algorithm = metadataList [ 0 ] . algo
681+ // If the algorithm is sha512, then it is the strongest
682+ // and we can return immediately
683+ if ( algorithm [ 3 ] === '5' ) {
684+ return algorithm
685+ }
686+
687+ for ( let i = 1 ; i < metadataList . length ; ++ i ) {
688+ const metadata = metadataList [ i ]
689+ // If the algorithm is sha512, then it is the strongest
690+ // and we can break the loop immediately
691+ if ( metadata . algo [ 3 ] === '5' ) {
692+ algorithm = 'sha512'
693+ break
694+ // If the algorithm is sha384, then a potential sha256 or sha384 is ignored
695+ } else if ( algorithm [ 3 ] === '3' ) {
696+ continue
697+ // algorithm is sha256, check if algorithm is sha384 and if so, set it as
698+ // the strongest
699+ } else if ( metadata . algo [ 3 ] === '3' ) {
700+ algorithm = 'sha384'
701+ }
702+ }
703+ return algorithm
704+ }
705+
706+ function filterMetadataListByAlgorithm ( metadataList , algorithm ) {
707+ if ( metadataList . length === 1 ) {
708+ return metadataList
709+ }
710+
711+ let pos = 0
712+ for ( let i = 0 ; i < metadataList . length ; ++ i ) {
713+ if ( metadataList [ i ] . algo === algorithm ) {
714+ metadataList [ pos ++ ] = metadataList [ i ]
715+ }
716+ }
717+
718+ metadataList . length = pos
719+
720+ return metadataList
721+ }
722+
723+ /**
724+ * Compares two base64 strings, allowing for base64url
725+ * in the second string.
726+ *
727+ * @param {string } actualValue always base64
728+ * @param {string } expectedValue base64 or base64url
729+ * @returns {boolean }
730+ */
731+ function compareBase64Mixed ( actualValue , expectedValue ) {
732+ if ( actualValue . length !== expectedValue . length ) {
733+ return false
734+ }
735+ for ( let i = 0 ; i < actualValue . length ; ++ i ) {
736+ if ( actualValue [ i ] !== expectedValue [ i ] ) {
737+ if (
738+ ( actualValue [ i ] === '+' && expectedValue [ i ] === '-' ) ||
739+ ( actualValue [ i ] === '/' && expectedValue [ i ] === '_' )
740+ ) {
741+ continue
742+ }
743+ return false
744+ }
745+ }
746+
747+ return true
748+ }
749+
678750// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
679751function tryUpgradeRequestToAPotentiallyTrustworthyURL ( request ) {
680752 // TODO
0 commit comments