@@ -7,14 +7,18 @@ const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
77const assert = require ( 'assert' )
88const { isUint8Array } = require ( 'util/types' )
99
10+ let supportedHashes = [ ]
11+
1012// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
1113/** @type {import('crypto')|undefined } */
1214let crypto
1315
1416try {
1517 crypto = require ( 'crypto' )
18+ const possibleRelevantHashes = [ 'sha256' , 'sha384' , 'sha512' ]
19+ supportedHashes = crypto . getHashes ( ) . filter ( ( hash ) => possibleRelevantHashes . includes ( hash ) )
20+ /* c8 ignore next 3 */
1621} catch {
17-
1822}
1923
2024function responseURL ( response ) {
@@ -542,66 +546,56 @@ function bytesMatch (bytes, metadataList) {
542546 return true
543547 }
544548
545- // 3. If parsedMetadata is the empty set, return true.
549+ // 3. If response is not eligible for integrity validation, return false.
550+ // TODO
551+
552+ // 4. If parsedMetadata is the empty set, return true.
546553 if ( parsedMetadata . length === 0 ) {
547554 return true
548555 }
549556
550- // 4 . Let metadata be the result of getting the strongest
557+ // 5 . Let metadata be the result of getting the strongest
551558 // metadata from parsedMetadata.
552- const list = parsedMetadata . sort ( ( c , d ) => d . algo . localeCompare ( c . algo ) )
553- // get the strongest algorithm
554- const strongest = list [ 0 ] . algo
555- // get all entries that use the strongest algorithm; ignore weaker
556- const metadata = list . filter ( ( item ) => item . algo === strongest )
559+ const strongest = getStrongestMetadata ( parsedMetadata )
560+ const metadata = filterMetadataListByAlgorithm ( parsedMetadata , strongest )
557561
558- // 5 . For each item in metadata:
562+ // 6 . For each item in metadata:
559563 for ( const item of metadata ) {
560564 // 1. Let algorithm be the alg component of item.
561565 const algorithm = item . algo
562566
563567 // 2. Let expectedValue be the val component of item.
564- let expectedValue = item . hash
568+ const expectedValue = item . hash
565569
566570 // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e
567571 // "be liberal with padding". This is annoying, and it's not even in the spec.
568572
569- if ( expectedValue . endsWith ( '==' ) ) {
570- expectedValue = expectedValue . slice ( 0 , - 2 )
571- }
572-
573573 // 3. Let actualValue be the result of applying algorithm to bytes.
574574 let actualValue = crypto . createHash ( algorithm ) . update ( bytes ) . digest ( 'base64' )
575575
576- if ( actualValue . endsWith ( '==' ) ) {
577- actualValue = actualValue . slice ( 0 , - 2 )
576+ if ( actualValue [ actualValue . length - 1 ] === '=' ) {
577+ if ( actualValue [ actualValue . length - 2 ] === '=' ) {
578+ actualValue = actualValue . slice ( 0 , - 2 )
579+ } else {
580+ actualValue = actualValue . slice ( 0 , - 1 )
581+ }
578582 }
579583
580584 // 4. If actualValue is a case-sensitive match for expectedValue,
581585 // return true.
582- if ( actualValue === expectedValue ) {
583- return true
584- }
585-
586- let actualBase64URL = crypto . createHash ( algorithm ) . update ( bytes ) . digest ( 'base64url' )
587-
588- if ( actualBase64URL . endsWith ( '==' ) ) {
589- actualBase64URL = actualBase64URL . slice ( 0 , - 2 )
590- }
591-
592- if ( actualBase64URL === expectedValue ) {
586+ if ( compareBase64Mixed ( actualValue , expectedValue ) ) {
593587 return true
594588 }
595589 }
596590
597- // 6 . Return false.
591+ // 7 . Return false.
598592 return false
599593}
600594
601595// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options
602596// https://www.w3.org/TR/CSP2/#source-list-syntax
603597// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
604- const parseHashWithOptions = / ( ( ?<algo > s h a 2 5 6 | s h a 3 8 4 | s h a 5 1 2 ) - (?< hash > [ A - z 0 - 9 + / ] { 1 } . * = { 0 , 2 } ) ) ( + [ \x21 - \x7e ] ? ) ? / i
598+ 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
605599
606600/**
607601 * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
@@ -615,8 +609,6 @@ function parseMetadata (metadata) {
615609 // 2. Let empty be equal to true.
616610 let empty = true
617611
618- const supportedHashes = crypto . getHashes ( )
619-
620612 // 3. For each token returned by splitting metadata on spaces:
621613 for ( const token of metadata . split ( ' ' ) ) {
622614 // 1. Set empty to false.
@@ -626,7 +618,11 @@ function parseMetadata (metadata) {
626618 const parsedToken = parseHashWithOptions . exec ( token )
627619
628620 // 3. If token does not parse, continue to the next token.
629- if ( parsedToken === null || parsedToken . groups === undefined ) {
621+ if (
622+ parsedToken === null ||
623+ parsedToken . groups === undefined ||
624+ parsedToken . groups . algo === undefined
625+ ) {
630626 // Note: Chromium blocks the request at this point, but Firefox
631627 // gives a warning that an invalid integrity was given. The
632628 // correct behavior is to ignore these, and subsequently not
@@ -635,11 +631,11 @@ function parseMetadata (metadata) {
635631 }
636632
637633 // 4. Let algorithm be the hash-algo component of token.
638- const algorithm = parsedToken . groups . algo
634+ const algorithm = parsedToken . groups . algo . toLowerCase ( )
639635
640636 // 5. If algorithm is a hash function recognized by the user
641637 // agent, add the parsed token to result.
642- if ( supportedHashes . includes ( algorithm . toLowerCase ( ) ) ) {
638+ if ( supportedHashes . includes ( algorithm ) ) {
643639 result . push ( parsedToken . groups )
644640 }
645641 }
@@ -652,6 +648,82 @@ function parseMetadata (metadata) {
652648 return result
653649}
654650
651+ /**
652+ * @param {{ algo: 'sha256' | 'sha384' | 'sha512' }[] } metadataList
653+ */
654+ function getStrongestMetadata ( metadataList ) {
655+ // Let algorithm be the algo component of the first item in metadataList.
656+ // Can be sha256
657+ let algorithm = metadataList [ 0 ] . algo
658+ // If the algorithm is sha512, then it is the strongest
659+ // and we can return immediately
660+ if ( algorithm [ 3 ] === '5' ) {
661+ return algorithm
662+ }
663+
664+ for ( let i = 1 ; i < metadataList . length ; ++ i ) {
665+ const metadata = metadataList [ i ]
666+ // If the algorithm is sha512, then it is the strongest
667+ // and we can break the loop immediately
668+ if ( metadata . algo [ 3 ] === '5' ) {
669+ algorithm = 'sha512'
670+ break
671+ // If the algorithm is sha384, then a potential sha256 or sha384 is ignored
672+ } else if ( algorithm [ 3 ] === '3' ) {
673+ continue
674+ // algorithm is sha256, check if algorithm is sha384 and if so, set it as
675+ // the strongest
676+ } else if ( metadata . algo [ 3 ] === '3' ) {
677+ algorithm = 'sha384'
678+ }
679+ }
680+ return algorithm
681+ }
682+
683+ function filterMetadataListByAlgorithm ( metadataList , algorithm ) {
684+ if ( metadataList . length === 1 ) {
685+ return metadataList
686+ }
687+
688+ let pos = 0
689+ for ( let i = 0 ; i < metadataList . length ; ++ i ) {
690+ if ( metadataList [ i ] . algo === algorithm ) {
691+ metadataList [ pos ++ ] = metadataList [ i ]
692+ }
693+ }
694+
695+ metadataList . length = pos
696+
697+ return metadataList
698+ }
699+
700+ /**
701+ * Compares two base64 strings, allowing for base64url
702+ * in the second string.
703+ *
704+ * @param {string } actualValue always base64
705+ * @param {string } expectedValue base64 or base64url
706+ * @returns {boolean }
707+ */
708+ function compareBase64Mixed ( actualValue , expectedValue ) {
709+ if ( actualValue . length !== expectedValue . length ) {
710+ return false
711+ }
712+ for ( let i = 0 ; i < actualValue . length ; ++ i ) {
713+ if ( actualValue [ i ] !== expectedValue [ i ] ) {
714+ if (
715+ ( actualValue [ i ] === '+' && expectedValue [ i ] === '-' ) ||
716+ ( actualValue [ i ] === '/' && expectedValue [ i ] === '_' )
717+ ) {
718+ continue
719+ }
720+ return false
721+ }
722+ }
723+
724+ return true
725+ }
726+
655727// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
656728function tryUpgradeRequestToAPotentiallyTrustworthyURL ( request ) {
657729 // TODO
@@ -1067,5 +1139,6 @@ module.exports = {
10671139 urlHasHttpsScheme,
10681140 urlIsHttpHttpsScheme,
10691141 readAllBytes,
1070- normalizeMethodRecord
1142+ normalizeMethodRecord,
1143+ parseMetadata
10711144}
0 commit comments