@@ -337,7 +337,14 @@ pub mod runtime {
337337 /// Require no SIMD candidate to be visible to this build and target.
338338 NoDetectedSimdCandidate ,
339339 /// Require scalar execution, the `simd` feature disabled, no detected
340- /// SIMD candidate, and the unsafe boundary enforced.
340+ /// SIMD candidate, the unsafe boundary enforced, and a CT result gate
341+ /// classified as a native hardware speculation barrier.
342+ ///
343+ /// This policy intentionally rejects targets that report only an
344+ /// ordering fence or compiler fence for the CT result gate. On `AArch64`,
345+ /// the reported hardware barrier means the crate emitted `isb sy` plus
346+ /// the CSDB hint; deployments must still attest whether that hint is
347+ /// effective on their specific core.
341348 HighAssuranceScalarOnly ,
342349 }
343350
@@ -492,6 +499,10 @@ pub mod runtime {
492499 && !self . simd_feature_enabled
493500 && !self . accelerated_backend_active
494501 && self . unsafe_boundary_enforced
502+ && matches ! (
503+ self . ct_gate_posture,
504+ CtGatePosture :: HardwareSpeculationBarrier
505+ )
495506 }
496507 }
497508 }
@@ -2845,7 +2856,9 @@ impl LineWrap {
28452856 ///
28462857 /// Panics when `line_len` is zero. Base64 wrapping requires a non-zero
28472858 /// encoded line length; accepting zero would make progress impossible for
2848- /// wrapped encoders.
2859+ /// wrapped encoders. This constructor is callable at runtime, so do not
2860+ /// pass attacker-controlled or externally configured values here; use
2861+ /// [`Self::checked_new`] for those cases.
28492862 #[ must_use]
28502863 pub const fn new ( line_len : usize , line_ending : LineEnding ) -> Self {
28512864 assert ! ( line_len != 0 , "base64 line wrap length must be non-zero" ) ;
@@ -3093,6 +3106,12 @@ impl<const CAP: usize> ExposedEncodedArray<CAP> {
30933106 /// This is an unprotected escape hatch. The returned array will not be
30943107 /// cleared by this crate on drop. Callers must clear it with their own
30953108 /// approved zeroization policy.
3109+ ///
3110+ /// # Security
3111+ ///
3112+ /// Treat this as a cleanup-boundary API. Failing to clear the returned
3113+ /// array leaves the encoded bytes in ordinary caller-owned memory until
3114+ /// overwritten by later stack or heap activity.
30963115 #[ must_use = "caller must zeroize the returned array" ]
30973116 pub fn into_exposed_unprotected_array_caller_must_zeroize ( mut self ) -> ( [ u8 ; CAP ] , usize ) {
30983117 let len = self . len ;
@@ -3410,6 +3429,12 @@ impl<const CAP: usize> ExposedDecodedArray<CAP> {
34103429 /// This is an unprotected escape hatch. The returned array will not be
34113430 /// cleared by this crate on drop. Callers must clear it with their own
34123431 /// approved zeroization policy.
3432+ ///
3433+ /// # Security
3434+ ///
3435+ /// Treat this as a cleanup-boundary API. Failing to clear the returned
3436+ /// array leaves decoded bytes, which may be secret-bearing, in ordinary
3437+ /// caller-owned memory until overwritten by later stack or heap activity.
34133438 #[ must_use = "caller must zeroize the returned array" ]
34143439 pub fn into_exposed_unprotected_array_caller_must_zeroize ( mut self ) -> ( [ u8 ; CAP ] , usize ) {
34153440 let len = self . len ;
@@ -3756,6 +3781,32 @@ impl Drop for ExposedSecretVec {
37563781 }
37573782}
37583783
3784+ #[ cfg( feature = "alloc" ) ]
3785+ struct WipeVecGuard {
3786+ bytes : alloc:: vec:: Vec < u8 > ,
3787+ }
3788+
3789+ #[ cfg( feature = "alloc" ) ]
3790+ impl WipeVecGuard {
3791+ fn from_vec ( bytes : alloc:: vec:: Vec < u8 > ) -> Self {
3792+ Self { bytes }
3793+ }
3794+
3795+ fn into_validated_secret_string ( mut self ) -> alloc:: string:: String {
3796+ wipe_vec_spare_capacity ( & mut self . bytes ) ;
3797+ let bytes = core:: mem:: take ( & mut self . bytes ) ;
3798+ core:: mem:: forget ( self ) ;
3799+ string_from_validated_secret_bytes ( bytes)
3800+ }
3801+ }
3802+
3803+ #[ cfg( feature = "alloc" ) ]
3804+ impl Drop for WipeVecGuard {
3805+ fn drop ( & mut self ) {
3806+ wipe_vec_all ( & mut self . bytes ) ;
3807+ }
3808+ }
3809+
37593810#[ cfg( feature = "alloc" ) ]
37603811impl AsRef < [ u8 ] > for ExposedSecretVec {
37613812 fn as_ref ( & self ) -> & [ u8 ] {
@@ -3945,15 +3996,13 @@ impl SecretBuffer {
39453996 return Err ( self ) ;
39463997 }
39473998
3948- // Security invariant: do not add fallible or allocating work between
3949- // taking `exposed.bytes` and wrapping it as `ExposedSecretString`.
3950- // During that narrow move-only window the bytes are temporarily in a
3951- // plain `Vec<u8>`.
3999+ // Keep the bytes behind a wiping guard until the final infallible
4000+ // ownership transfer into `String`.
39524001 let mut exposed = self . into_exposed_vec ( ) ;
3953- let bytes = core:: mem:: take ( & mut exposed. bytes ) ;
4002+ let guard = WipeVecGuard :: from_vec ( core:: mem:: take ( & mut exposed. bytes ) ) ;
39544003 drop ( exposed) ;
39554004 Ok ( ExposedSecretString :: from_string (
3956- string_from_validated_secret_bytes ( bytes ) ,
4005+ guard . into_validated_secret_string ( ) ,
39574006 ) )
39584007 }
39594008
@@ -6892,6 +6941,13 @@ pub enum DecodeError {
68926941 /// Available output bytes.
68936942 available : usize ,
68946943 } ,
6944+ /// The caller-provided constant-time staging buffer is too small.
6945+ StagingTooSmall {
6946+ /// Required staging bytes.
6947+ required : usize ,
6948+ /// Available staging bytes.
6949+ available : usize ,
6950+ } ,
68956951}
68966952
68976953impl core:: fmt:: Display for DecodeError {
@@ -6913,6 +6969,13 @@ impl core::fmt::Display for DecodeError {
69136969 f,
69146970 "base64 decode output buffer too small: required {required}, available {available}"
69156971 ) ,
6972+ Self :: StagingTooSmall {
6973+ required,
6974+ available,
6975+ } => write ! (
6976+ f,
6977+ "base64 decode staging buffer too small: required {required}, available {available}"
6978+ ) ,
69166979 }
69176980 }
69186981}
@@ -6930,27 +6993,51 @@ impl DecodeError {
69306993 Self :: InvalidLineWrap { index } => Self :: InvalidLineWrap {
69316994 index : index + offset,
69326995 } ,
6933- Self :: InvalidInput | Self :: InvalidLength | Self :: OutputTooSmall { .. } => self ,
6996+ Self :: InvalidInput
6997+ | Self :: InvalidLength
6998+ | Self :: OutputTooSmall { .. }
6999+ | Self :: StagingTooSmall { .. } => self ,
69347000 }
69357001 }
69367002}
69377003
69387004#[ cfg( feature = "std" ) ]
69397005impl std:: error:: Error for DecodeError { }
69407006
7007+ struct LegacyBytes < ' a > {
7008+ input : & ' a [ u8 ] ,
7009+ index : usize ,
7010+ }
7011+
7012+ impl < ' a > LegacyBytes < ' a > {
7013+ const fn new ( input : & ' a [ u8 ] ) -> Self {
7014+ Self { input, index : 0 }
7015+ }
7016+
7017+ fn next_byte ( & mut self ) -> Option < ( usize , u8 ) > {
7018+ while self . index < self . input . len ( ) {
7019+ let index = self . index ;
7020+ let byte = self . input [ index] ;
7021+ self . index += 1 ;
7022+ if !is_legacy_whitespace ( byte) {
7023+ return Some ( ( index, byte) ) ;
7024+ }
7025+ }
7026+ None
7027+ }
7028+ }
7029+
69417030fn validate_legacy_decode < A : Alphabet , const PAD : bool > (
69427031 input : & [ u8 ] ,
69437032) -> Result < usize , DecodeError > {
7033+ let mut bytes = LegacyBytes :: new ( input) ;
69447034 let mut chunk = [ 0u8 ; 4 ] ;
69457035 let mut indexes = [ 0usize ; 4 ] ;
69467036 let mut chunk_len = 0 ;
69477037 let mut required = 0 ;
69487038 let mut terminal_seen = false ;
69497039
6950- for ( index, byte) in input. iter ( ) . copied ( ) . enumerate ( ) {
6951- if is_legacy_whitespace ( byte) {
6952- continue ;
6953- }
7040+ while let Some ( ( index, byte) ) = bytes. next_byte ( ) {
69547041 if terminal_seen {
69557042 return Err ( DecodeError :: InvalidPadding { index } ) ;
69567043 }
@@ -6984,16 +7071,14 @@ fn decode_legacy_to_slice<A: Alphabet, const PAD: bool>(
69847071 input : & [ u8 ] ,
69857072 output : & mut [ u8 ] ,
69867073) -> Result < usize , DecodeError > {
7074+ let mut bytes = LegacyBytes :: new ( input) ;
69877075 let mut chunk = [ 0u8 ; 4 ] ;
69887076 let mut indexes = [ 0usize ; 4 ] ;
69897077 let mut chunk_len = 0 ;
69907078 let mut write = 0 ;
69917079 let mut terminal_seen = false ;
69927080
6993- for ( index, byte) in input. iter ( ) . copied ( ) . enumerate ( ) {
6994- if is_legacy_whitespace ( byte) {
6995- continue ;
6996- }
7081+ while let Some ( ( index, byte) ) = bytes. next_byte ( ) {
69977082 if terminal_seen {
69987083 return Err ( DecodeError :: InvalidPadding { index } ) ;
69997084 }
@@ -7095,7 +7180,9 @@ impl<'a> WrappedBytes<'a> {
70957180
70967181 fn starts_with_line_ending ( & self ) -> bool {
70977182 let line_ending = self . wrap . line_ending . as_bytes ( ) ;
7098- let end = self . index + line_ending. len ( ) ;
7183+ let Some ( end) = self . index . checked_add ( line_ending. len ( ) ) else {
7184+ return false ;
7185+ } ;
70997186 end <= self . input . len ( ) && & self . input [ self . index ..end] == line_ending
71007187 }
71017188}
@@ -7230,7 +7317,8 @@ fn map_chunk_error(err: DecodeError, indexes: &[usize; 4]) -> DecodeError {
72307317 DecodeError :: InvalidInput
72317318 | DecodeError :: InvalidLineWrap { .. }
72327319 | DecodeError :: InvalidLength
7233- | DecodeError :: OutputTooSmall { .. } => err,
7320+ | DecodeError :: OutputTooSmall { .. }
7321+ | DecodeError :: StagingTooSmall { .. } => err,
72347322 }
72357323}
72367324
@@ -7248,7 +7336,8 @@ fn map_partial_chunk_error(err: DecodeError, indexes: &[usize; 4], len: usize) -
72487336 | DecodeError :: InvalidLineWrap { .. }
72497337 | DecodeError :: InvalidInput
72507338 | DecodeError :: InvalidLength
7251- | DecodeError :: OutputTooSmall { .. } => err,
7339+ | DecodeError :: OutputTooSmall { .. }
7340+ | DecodeError :: StagingTooSmall { .. } => err,
72527341 }
72537342}
72547343
@@ -7632,7 +7721,7 @@ fn ct_decode_slice_staged_clear_tail<A: Alphabet, const PAD: bool>(
76327721 if staging. len ( ) < required {
76337722 wipe_bytes ( output) ;
76347723 wipe_bytes ( staging) ;
7635- return Err ( DecodeError :: OutputTooSmall {
7724+ return Err ( DecodeError :: StagingTooSmall {
76367725 required,
76377726 available : staging. len ( ) ,
76387727 } ) ;
0 commit comments