@@ -7,14 +7,18 @@ const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
7
7
const assert = require ( 'assert' )
8
8
const { isUint8Array } = require ( 'util/types' )
9
9
10
+ let supportedHashes = [ ]
11
+
10
12
// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
11
13
/** @type {import('crypto')|undefined } */
12
14
let crypto
13
15
14
16
try {
15
17
crypto = require ( 'crypto' )
18
+ const possibleRelevantHashes = [ 'sha256' , 'sha384' , 'sha512' ]
19
+ supportedHashes = crypto . getHashes ( ) . filter ( ( hash ) => possibleRelevantHashes . includes ( hash ) )
20
+ /* c8 ignore next 3 */
16
21
} catch {
17
-
18
22
}
19
23
20
24
function responseURL ( response ) {
@@ -542,66 +546,56 @@ function bytesMatch (bytes, metadataList) {
542
546
return true
543
547
}
544
548
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.
546
553
if ( parsedMetadata . length === 0 ) {
547
554
return true
548
555
}
549
556
550
- // 4 . Let metadata be the result of getting the strongest
557
+ // 5 . Let metadata be the result of getting the strongest
551
558
// 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 )
557
561
558
- // 5 . For each item in metadata:
562
+ // 6 . For each item in metadata:
559
563
for ( const item of metadata ) {
560
564
// 1. Let algorithm be the alg component of item.
561
565
const algorithm = item . algo
562
566
563
567
// 2. Let expectedValue be the val component of item.
564
- let expectedValue = item . hash
568
+ const expectedValue = item . hash
565
569
566
570
// See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e
567
571
// "be liberal with padding". This is annoying, and it's not even in the spec.
568
572
569
- if ( expectedValue . endsWith ( '==' ) ) {
570
- expectedValue = expectedValue . slice ( 0 , - 2 )
571
- }
572
-
573
573
// 3. Let actualValue be the result of applying algorithm to bytes.
574
574
let actualValue = crypto . createHash ( algorithm ) . update ( bytes ) . digest ( 'base64' )
575
575
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
+ }
578
582
}
579
583
580
584
// 4. If actualValue is a case-sensitive match for expectedValue,
581
585
// 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 ) ) {
593
587
return true
594
588
}
595
589
}
596
590
597
- // 6 . Return false.
591
+ // 7 . Return false.
598
592
return false
599
593
}
600
594
601
595
// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options
602
596
// https://www.w3.org/TR/CSP2/#source-list-syntax
603
597
// 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
605
599
606
600
/**
607
601
* @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
@@ -615,8 +609,6 @@ function parseMetadata (metadata) {
615
609
// 2. Let empty be equal to true.
616
610
let empty = true
617
611
618
- const supportedHashes = crypto . getHashes ( )
619
-
620
612
// 3. For each token returned by splitting metadata on spaces:
621
613
for ( const token of metadata . split ( ' ' ) ) {
622
614
// 1. Set empty to false.
@@ -626,7 +618,11 @@ function parseMetadata (metadata) {
626
618
const parsedToken = parseHashWithOptions . exec ( token )
627
619
628
620
// 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
+ ) {
630
626
// Note: Chromium blocks the request at this point, but Firefox
631
627
// gives a warning that an invalid integrity was given. The
632
628
// correct behavior is to ignore these, and subsequently not
@@ -635,11 +631,11 @@ function parseMetadata (metadata) {
635
631
}
636
632
637
633
// 4. Let algorithm be the hash-algo component of token.
638
- const algorithm = parsedToken . groups . algo
634
+ const algorithm = parsedToken . groups . algo . toLowerCase ( )
639
635
640
636
// 5. If algorithm is a hash function recognized by the user
641
637
// agent, add the parsed token to result.
642
- if ( supportedHashes . includes ( algorithm . toLowerCase ( ) ) ) {
638
+ if ( supportedHashes . includes ( algorithm ) ) {
643
639
result . push ( parsedToken . groups )
644
640
}
645
641
}
@@ -652,6 +648,82 @@ function parseMetadata (metadata) {
652
648
return result
653
649
}
654
650
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
+
655
727
// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
656
728
function tryUpgradeRequestToAPotentiallyTrustworthyURL ( request ) {
657
729
// TODO
@@ -1067,5 +1139,6 @@ module.exports = {
1067
1139
urlHasHttpsScheme,
1068
1140
urlIsHttpHttpsScheme,
1069
1141
readAllBytes,
1070
- normalizeMethodRecord
1142
+ normalizeMethodRecord,
1143
+ parseMetadata
1071
1144
}
0 commit comments