@@ -11,11 +11,15 @@ const assert = require('node:assert')
11
11
const { isUint8Array } = require ( 'node:util/types' )
12
12
const { webidl } = require ( './webidl' )
13
13
14
+ let supportedHashes = [ ]
15
+
14
16
// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
15
17
/** @type {import('crypto') } */
16
18
let crypto
17
19
try {
18
20
crypto = require ( 'node:crypto' )
21
+ const possibleRelevantHashes = [ 'sha256' , 'sha384' , 'sha512' ]
22
+ supportedHashes = crypto . getHashes ( ) . filter ( ( hash ) => possibleRelevantHashes . includes ( hash ) )
19
23
/* c8 ignore next 3 */
20
24
} catch {
21
25
@@ -565,66 +569,56 @@ function bytesMatch (bytes, metadataList) {
565
569
return true
566
570
}
567
571
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.
569
576
if ( parsedMetadata . length === 0 ) {
570
577
return true
571
578
}
572
579
573
- // 4 . Let metadata be the result of getting the strongest
580
+ // 5 . Let metadata be the result of getting the strongest
574
581
// 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 )
580
584
581
- // 5 . For each item in metadata:
585
+ // 6 . For each item in metadata:
582
586
for ( const item of metadata ) {
583
587
// 1. Let algorithm be the alg component of item.
584
588
const algorithm = item . algo
585
589
586
590
// 2. Let expectedValue be the val component of item.
587
- let expectedValue = item . hash
591
+ const expectedValue = item . hash
588
592
589
593
// See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e
590
594
// "be liberal with padding". This is annoying, and it's not even in the spec.
591
595
592
- if ( expectedValue . endsWith ( '==' ) ) {
593
- expectedValue = expectedValue . slice ( 0 , - 2 )
594
- }
595
-
596
596
// 3. Let actualValue be the result of applying algorithm to bytes.
597
597
let actualValue = crypto . createHash ( algorithm ) . update ( bytes ) . digest ( 'base64' )
598
598
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
+ }
601
605
}
602
606
603
607
// 4. If actualValue is a case-sensitive match for expectedValue,
604
608
// 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 ) ) {
616
610
return true
617
611
}
618
612
}
619
613
620
- // 6 . Return false.
614
+ // 7 . Return false.
621
615
return false
622
616
}
623
617
624
618
// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options
625
619
// https://www.w3.org/TR/CSP2/#source-list-syntax
626
620
// 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
628
622
629
623
/**
630
624
* @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
@@ -638,8 +632,6 @@ function parseMetadata (metadata) {
638
632
// 2. Let empty be equal to true.
639
633
let empty = true
640
634
641
- const supportedHashes = crypto . getHashes ( )
642
-
643
635
// 3. For each token returned by splitting metadata on spaces:
644
636
for ( const token of metadata . split ( ' ' ) ) {
645
637
// 1. Set empty to false.
@@ -649,7 +641,11 @@ function parseMetadata (metadata) {
649
641
const parsedToken = parseHashWithOptions . exec ( token )
650
642
651
643
// 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
+ ) {
653
649
// Note: Chromium blocks the request at this point, but Firefox
654
650
// gives a warning that an invalid integrity was given. The
655
651
// correct behavior is to ignore these, and subsequently not
@@ -658,11 +654,11 @@ function parseMetadata (metadata) {
658
654
}
659
655
660
656
// 4. Let algorithm be the hash-algo component of token.
661
- const algorithm = parsedToken . groups . algo
657
+ const algorithm = parsedToken . groups . algo . toLowerCase ( )
662
658
663
659
// 5. If algorithm is a hash function recognized by the user
664
660
// agent, add the parsed token to result.
665
- if ( supportedHashes . includes ( algorithm . toLowerCase ( ) ) ) {
661
+ if ( supportedHashes . includes ( algorithm ) ) {
666
662
result . push ( parsedToken . groups )
667
663
}
668
664
}
@@ -675,6 +671,82 @@ function parseMetadata (metadata) {
675
671
return result
676
672
}
677
673
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
+
678
750
// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
679
751
function tryUpgradeRequestToAPotentiallyTrustworthyURL ( request ) {
680
752
// TODO
0 commit comments