@@ -440,18 +440,18 @@ private void ValidateProducedDebContent(
440440
441441 using MD5 md5 = MD5 . Create ( ) ;
442442 using FileStream fileStream = File . OpenRead ( layoutFilePath ) ;
443- string newHash = Convert . ToHexString ( md5 . ComputeHash ( fileStream ) ) ;
443+ string newHash = Convert . ToHexStringLower ( md5 . ComputeHash ( fileStream ) ) ;
444444
445445 if ( signableFiles . Contains ( targetSystemFilePath ) )
446446 {
447447 newHash . Should ( ) . NotBe ( originalHash ) ;
448- md5sumsContents . Should ( ) . Contain ( $ "{ newHash } { targetSystemFilePath } ") ;
449- md5sumsContents . Should ( ) . NotContain ( $ "{ originalHash } { targetSystemFilePath } ") ;
448+ md5sumsContents . Should ( ) . Contain ( $ "{ newHash } { targetSystemFilePath } ") ;
449+ md5sumsContents . Should ( ) . NotContain ( $ "{ originalHash } { targetSystemFilePath } ") ;
450450 }
451451 else
452452 {
453453 newHash . Should ( ) . Be ( originalHash ) ;
454- md5sumsContents . Should ( ) . Contain ( $ "{ originalHash } { targetSystemFilePath } ") ;
454+ md5sumsContents . Should ( ) . Contain ( $ "{ originalHash } { targetSystemFilePath } ") ;
455455 }
456456 }
457457
@@ -1831,8 +1831,8 @@ public void CheckDebSigning()
18311831
18321832 var expectedFilesOriginalHashes = new ( string , string ) [ ]
18331833 {
1834- ( "usr/local/bin/hello" , "644981BBD6F4ED1B3CF68CD0F47981AA " ) ,
1835- ( "usr/local/bin/mscorlib.dll" , "B80EEBA2B8616B7C37E49B004D69BBB7 " )
1834+ ( "usr/local/bin/hello" , "644981bbd6f4ed1b3cf68cd0f47981aa " ) ,
1835+ ( "usr/local/bin/mscorlib.dll" , "b80eeba2b8616b7c37e49b004d69bbb7 " )
18361836 } ;
18371837 string [ ] signableFiles = [ "usr/local/bin/mscorlib.dll" ] ;
18381838 string expectedControlFileContent = "Package: test\n Version: 1.0\n Section: base\n Priority: optional\n Architecture: all\n " ;
@@ -3635,5 +3635,132 @@ public void ContainerSigningWithDoNotUnpackViaFileExtensionSignInfo()
36353635 "File 'NestedContainer.1.0.0.nupkg' Certificate='NuGet'" ,
36363636 } ) ;
36373637 }
3638+
3639+ [ Fact ]
3640+ public void NotarizationRetriesOnFailure ( )
3641+ {
3642+ // List of files to be considered for signing
3643+ var itemsToSign = new List < ItemToSign > ( )
3644+ {
3645+ new ItemToSign ( GetResourcePath ( "test.pkg" ) )
3646+ } ;
3647+
3648+ // Default signing information
3649+ var strongNameSignInfo = new Dictionary < string , List < SignInfo > > ( )
3650+ {
3651+ { "581d91ccdfc4ea9c" , new List < SignInfo > { new SignInfo ( certificate : "ArcadeCertTest" , strongName : "ArcadeStrongTest" ) } }
3652+ } ;
3653+
3654+ // Set up the cert to allow for signing and notarization.
3655+ var additionalCertificateInfo = new Dictionary < string , List < AdditionalCertificateInformation > > ( )
3656+ {
3657+ { "MacDeveloperHardenWithNotarization" ,
3658+ new List < AdditionalCertificateInformation > ( ) {
3659+ new AdditionalCertificateInformation ( ) { MacNotarizationAppName = "dotnet" , MacSigningOperation = "MacDeveloperHarden" }
3660+ }
3661+ }
3662+ } ;
3663+
3664+ // Overriding information
3665+ var fileSignInfo = new Dictionary < ExplicitSignInfoKey , FileSignInfoEntry > ( )
3666+ {
3667+ { new ExplicitSignInfoKey ( "test.pkg" ) , new FileSignInfoEntry ( "MacDeveloperHardenWithNotarization" ) }
3668+ } ;
3669+
3670+ var configuration = new Configuration ( _tmpDir ,
3671+ itemsToSign ,
3672+ strongNameSignInfo ,
3673+ fileSignInfo ,
3674+ s_fileExtensionSignInfo ,
3675+ additionalCertificateInfo ,
3676+ itemsToSkip3rdPartyCheck : null ,
3677+ tarToolPath : null ,
3678+ pkgToolPath : null ,
3679+ snPath : null ,
3680+ new TaskLoggingHelper ( new FakeBuildEngine ( _output ) , "SignToolTests" ) ,
3681+ telemetry : null ) ;
3682+
3683+ var parsedSigningInput = configuration . GenerateListOfFiles ( ) ;
3684+
3685+ // Create a fake build engine to track build calls
3686+ var fakeBuildEngine = new FakeBuildEngineWithFailures ( _output , failNotarizationCount : 3 ) ;
3687+ var fakeLog = new TaskLoggingHelper ( fakeBuildEngine , "SignToolTests" ) ;
3688+
3689+ var args = new SignToolArgs (
3690+ tempPath : _tmpDir ,
3691+ microBuildCorePath : CreateTestResource ( "MicroBuild.Core" ) ,
3692+ testSign : true ,
3693+ dotnetPath : null ,
3694+ msbuildVerbosity : "quiet" ,
3695+ logDir : _tmpDir ,
3696+ enclosingDir : "" ,
3697+ snBinaryPath : null ,
3698+ wix3ToolsPath : null ,
3699+ wixToolsPath : null ,
3700+ tarToolPath : null ,
3701+ pkgToolPath : null ,
3702+ dotnetTimeout : - 1 ) ;
3703+
3704+ var signTool = new FakeSignTool ( args , fakeLog ) ;
3705+
3706+ var util = new BatchSignUtil (
3707+ fakeBuildEngine ,
3708+ fakeLog ,
3709+ signTool ,
3710+ parsedSigningInput ,
3711+ Array . Empty < string > ( ) ,
3712+ null ) ;
3713+
3714+ util . Go ( false ) ;
3715+
3716+ // Verify that notarization was retried
3717+ fakeBuildEngine . NotarizationAttempts . Should ( ) . Be ( 4 , "Notarization should succeed on the 4th attempt after 3 failures" ) ;
3718+ }
3719+ }
3720+
3721+ /// <summary>
3722+ /// Fake build engine that can simulate notarization failures
3723+ /// </summary>
3724+ internal class FakeBuildEngineWithFailures : IBuildEngine
3725+ {
3726+ private readonly int _failNotarizationCount ;
3727+ private int _notarizationAttemptsSoFar = 0 ;
3728+ private readonly FakeBuildEngine _innerEngine ;
3729+
3730+ public int NotarizationAttempts => _notarizationAttemptsSoFar ;
3731+
3732+ public FakeBuildEngineWithFailures ( ITestOutputHelper output , int failNotarizationCount )
3733+ {
3734+ _failNotarizationCount = failNotarizationCount ;
3735+ _innerEngine = new FakeBuildEngine ( output ) ;
3736+ }
3737+
3738+ public bool BuildProjectFile ( string projectFileName , string [ ] targetNames , System . Collections . IDictionary globalProperties , System . Collections . IDictionary targetOutputs )
3739+ {
3740+ // Check if this is a notarization project
3741+ if ( projectFileName . Contains ( "Notarize" ) )
3742+ {
3743+ _notarizationAttemptsSoFar ++ ;
3744+
3745+ // Fail the first N attempts
3746+ if ( _notarizationAttemptsSoFar <= _failNotarizationCount )
3747+ {
3748+ return false ;
3749+ }
3750+ }
3751+
3752+ // Otherwise use the inner engine implementation
3753+ return _innerEngine . BuildProjectFile ( projectFileName , targetNames , globalProperties , targetOutputs ) ;
3754+ }
3755+
3756+ public int ColumnNumberOfTaskNode => _innerEngine . ColumnNumberOfTaskNode ;
3757+ public bool ContinueOnError { get => _innerEngine . ContinueOnError ; set => _innerEngine . ContinueOnError = value ; }
3758+ public int LineNumberOfTaskNode => _innerEngine . LineNumberOfTaskNode ;
3759+ public string ProjectFileOfTaskNode => _innerEngine . ProjectFileOfTaskNode ;
3760+
3761+ public void LogCustomEvent ( CustomBuildEventArgs e ) => _innerEngine . LogCustomEvent ( e ) ;
3762+ public void LogErrorEvent ( BuildErrorEventArgs e ) => _innerEngine . LogErrorEvent ( e ) ;
3763+ public void LogMessageEvent ( BuildMessageEventArgs e ) => _innerEngine . LogMessageEvent ( e ) ;
3764+ public void LogWarningEvent ( BuildWarningEventArgs e ) => _innerEngine . LogWarningEvent ( e ) ;
36383765 }
36393766}
0 commit comments