diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index aaf5e2d43..797a42d73 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -157,46 +157,52 @@ extends: includeRootFolder: false archiveType: zip archiveFile: '$(Build.ArtifactStagingDirectory)\symbols\gcm-${{ dim.runtime }}-$(version)-symbols.zip' - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign payload' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)\payload' - pattern: | - **/*.exe - **/*.dll - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-230012", - "OperationCode": "SigntoolSign", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": { - "OpusName": "Microsoft", - "OpusInfo": "https://www.microsoft.com", - "FileDigest": "/fd SHA256", - "PageHash": "/NPH", - "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + - ${{ if eq(parameters.esrp, true) }}: + - task: EsrpCodeSigning@5 + displayName: 'Sign payload' + inputs: + connectedServiceName: '$(esrpAppConnectionName)' + useMSIAuthentication: true + appRegistrationClientId: '$(esrpClientId)' + appRegistrationTenantId: '$(esrpTenantId)' + authAkvName: '$(esrpKeyVaultName)' + authSignCertName: '$(esrpSignReqCertName)' + serviceEndpointUrl: '$(esrpEndpointUrl)' + folderPath: '$(Build.ArtifactStagingDirectory)\payload' + pattern: | + **/*.exe + **/*.dll + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "https://www.microsoft.com", + "FileDigest": "/fd SHA256", + "PageHash": "/NPH", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + }, + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolVerify", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": {} } - }, - { - "KeyCode": "CP-230012", - "OperationCode": "SigntoolVerify", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": {} - } - ] + ] + - task: PowerShell@2 + displayName: 'Clean up code signing artifacts' + inputs: + targetType: inline + script: | + Remove-Item "$(Build.ArtifactStagingDirectory)\payload\CodeSignSummary-*.md" - task: PowerShell@2 displayName: 'Build installers' inputs: @@ -209,46 +215,47 @@ extends: -p:PayloadPath="$(Build.ArtifactStagingDirectory)\payload" ` -p:OutputPath="$(Build.ArtifactStagingDirectory)\installers" ` -p:RuntimeIdentifier="${{ dim.runtime }}" - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign installers' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)\installers' - pattern: '**/*.exe' - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-230012", - "OperationCode": "SigntoolSign", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": { - "OpusName": "Microsoft", - "OpusInfo": "https://www.microsoft.com", - "FileDigest": "/fd SHA256", - "PageHash": "/NPH", - "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + - ${{ if eq(parameters.esrp, true) }}: + - task: EsrpCodeSigning@5 + condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) + displayName: 'Sign installers' + inputs: + connectedServiceName: '$(esrpAppConnectionName)' + useMSIAuthentication: true + appRegistrationClientId: '$(esrpClientId)' + appRegistrationTenantId: '$(esrpTenantId)' + authAkvName: '$(esrpKeyVaultName)' + authSignCertName: '$(esrpSignReqCertName)' + serviceEndpointUrl: '$(esrpEndpointUrl)' + folderPath: '$(Build.ArtifactStagingDirectory)\installers' + pattern: '**/*.exe' + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "https://www.microsoft.com", + "FileDigest": "/fd SHA256", + "PageHash": "/NPH", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + }, + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolVerify", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": {} } - }, - { - "KeyCode": "CP-230012", - "OperationCode": "SigntoolVerify", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": {} - } - ] + ] - task: ArchiveFiles@2 - displayName: 'Archive signed payload' + displayName: 'Archive payload' inputs: rootFolderOrFile: '$(Build.ArtifactStagingDirectory)\payload' includeRootFolder: false @@ -311,105 +318,105 @@ extends: archiveType: tar tarCompression: gz archiveFile: '$(Build.ArtifactStagingDirectory)/symbols/gcm-${{ dim.runtime }}-$(version)-symbols.tar.gz' - - task: AzureKeyVault@2 - displayName: 'Download developer certificate' - inputs: - azureSubscription: '$(esrpMIConnectionName)' - keyVaultName: '$(esrpKeyVaultName)' - secretsFilter: 'mac-developer-certificate,mac-developer-certificate-password,mac-developer-certificate-identity' - - task: Bash@3 - displayName: 'Import developer certificate' - inputs: - targetType: inline - script: | - # Create and unlock a keychain for the developer certificate - security create-keychain -p pwd $(Agent.TempDirectory)/buildagent.keychain - security default-keychain -s $(Agent.TempDirectory)/buildagent.keychain - security unlock-keychain -p pwd $(Agent.TempDirectory)/buildagent.keychain + - ${{ if eq(parameters.esrp, true) }}: + - task: AzureKeyVault@2 + displayName: 'Download developer certificate' + inputs: + azureSubscription: '$(esrpMIConnectionName)' + keyVaultName: '$(esrpKeyVaultName)' + secretsFilter: 'mac-developer-certificate,mac-developer-certificate-password,mac-developer-certificate-identity' + - task: Bash@3 + displayName: 'Import developer certificate' + inputs: + targetType: inline + script: | + # Create and unlock a keychain for the developer certificate + security create-keychain -p pwd $(Agent.TempDirectory)/buildagent.keychain + security default-keychain -s $(Agent.TempDirectory)/buildagent.keychain + security unlock-keychain -p pwd $(Agent.TempDirectory)/buildagent.keychain - echo $(mac-developer-certificate) | base64 -D > $(Agent.TempDirectory)/cert.p12 - echo $(mac-developer-certificate-password) > $(Agent.TempDirectory)/cert.password + echo $(mac-developer-certificate) | base64 -D > $(Agent.TempDirectory)/cert.p12 + echo $(mac-developer-certificate-password) > $(Agent.TempDirectory)/cert.password - # Import the developer certificate - security import $(Agent.TempDirectory)/cert.p12 \ - -k $(Agent.TempDirectory)/buildagent.keychain \ - -P "$(mac-developer-certificate-password)" \ - -T /usr/bin/codesign + # Import the developer certificate + security import $(Agent.TempDirectory)/cert.p12 \ + -k $(Agent.TempDirectory)/buildagent.keychain \ + -P "$(mac-developer-certificate-password)" \ + -T /usr/bin/codesign - # Clean up the cert file immediately after import - rm $(Agent.TempDirectory)/cert.p12 + # Clean up the cert file immediately after import + rm $(Agent.TempDirectory)/cert.p12 - # Set ACLs to allow codesign to access the private key - security set-key-partition-list \ - -S apple-tool:,apple:,codesign: \ - -s -k pwd \ - $(Agent.TempDirectory)/buildagent.keychain - - task: Bash@3 - displayName: 'Developer sign payload files' - inputs: - targetType: inline - script: | - mkdir -p $(Build.ArtifactStagingDirectory)/tosign/payload + # Set ACLs to allow codesign to access the private key + security set-key-partition-list \ + -S apple-tool:,apple:,codesign: \ + -s -k pwd \ + $(Agent.TempDirectory)/buildagent.keychain + - task: Bash@3 + displayName: 'Developer sign payload files' + inputs: + targetType: inline + script: | + mkdir -p $(Build.ArtifactStagingDirectory)/tosign/payload - # Copy the files that need signing (Mach-o executables and dylibs) - pushd $(Build.ArtifactStagingDirectory)/payload - find . -type f -exec file --mime {} + \ - | sed -n '/mach/s/: .*//p' \ - | while IFS= read -r f; do - rel="${f#./}" - tgt="$(Build.ArtifactStagingDirectory)/tosign/payload/$rel" - mkdir -p "$(dirname "$tgt")" - cp -- "$f" "$tgt" - done - popd + # Copy the files that need signing (Mach-o executables and dylibs) + pushd $(Build.ArtifactStagingDirectory)/payload + find . -type f -exec file --mime {} + \ + | sed -n '/mach/s/: .*//p' \ + | while IFS= read -r f; do + rel="${f#./}" + tgt="$(Build.ArtifactStagingDirectory)/tosign/payload/$rel" + mkdir -p "$(dirname "$tgt")" + cp -- "$f" "$tgt" + done + popd - # Developer sign the files - ./src/osx/Installer.Mac/codesign.sh \ - "$(Build.ArtifactStagingDirectory)/tosign/payload" \ - "$(mac-developer-certificate-identity)" \ - "$PWD/src/osx/Installer.Mac/entitlements.xml" - # ESRP code signing for macOS requires the files be packaged in a zip file for submission - - task: ArchiveFiles@2 - displayName: 'Archive files for signing' - inputs: - rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/tosign/payload' - includeRootFolder: false - archiveType: zip - archiveFile: '$(Build.ArtifactStagingDirectory)/tosign/payload.zip' - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign payload' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/tosign' - pattern: 'payload.zip' - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-401337-Apple", - "OperationCode": "MacAppDeveloperSign", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": { - "Hardening": "Enable" + # Developer sign the files + ./src/osx/Installer.Mac/codesign.sh \ + "$(Build.ArtifactStagingDirectory)/tosign/payload" \ + "$(mac-developer-certificate-identity)" \ + "$PWD/src/osx/Installer.Mac/entitlements.xml" + # ESRP code signing for macOS requires the files be packaged in a zip file for submission + - task: ArchiveFiles@2 + displayName: 'Archive files for signing' + inputs: + rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/tosign/payload' + includeRootFolder: false + archiveType: zip + archiveFile: '$(Build.ArtifactStagingDirectory)/tosign/payload.zip' + - task: EsrpCodeSigning@5 + displayName: 'Sign payload' + inputs: + connectedServiceName: '$(esrpAppConnectionName)' + useMSIAuthentication: true + appRegistrationClientId: '$(esrpClientId)' + appRegistrationTenantId: '$(esrpTenantId)' + authAkvName: '$(esrpKeyVaultName)' + authSignCertName: '$(esrpSignReqCertName)' + serviceEndpointUrl: '$(esrpEndpointUrl)' + folderPath: '$(Build.ArtifactStagingDirectory)/tosign' + pattern: 'payload.zip' + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-401337-Apple", + "OperationCode": "MacAppDeveloperSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "Hardening": "Enable" + } } - } - ] - # Extract signed files, overwriting the unsigned files, ready for packaging - - task: Bash@3 - displayName: 'Extract signed payload files' - inputs: - targetType: inline - script: | - unzip -uo $(Build.ArtifactStagingDirectory)/tosign/payload.zip -d $(Build.ArtifactStagingDirectory)/payload + ] + # Extract signed files, overwriting the unsigned files, ready for packaging + - task: Bash@3 + displayName: 'Extract signed payload files' + inputs: + targetType: inline + script: | + unzip -uo $(Build.ArtifactStagingDirectory)/tosign/payload.zip -d $(Build.ArtifactStagingDirectory)/payload - task: Bash@3 displayName: 'Build component package' inputs: @@ -428,94 +435,95 @@ extends: --version="$(version)" \ --runtime="${{ dim.runtime }}" \ --package-path="$(Build.ArtifactStagingDirectory)/pkg" \ - --output="$(Build.ArtifactStagingDirectory)/installers-presign/gcm-${{ dim.runtime }}-$(version).pkg" - # ESRP code signing for macOS requires the files be packaged in a zip file first - - task: Bash@3 - displayName: 'Prepare installer package for signing' - inputs: - targetType: inline - script: | - mkdir -p $(Build.ArtifactStagingDirectory)/tosign - cd $(Build.ArtifactStagingDirectory)/installers-presign - zip -rX $(Build.ArtifactStagingDirectory)/tosign/installers-presign.zip *.pkg - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign installer package' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/tosign' - pattern: 'installers-presign.zip' - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-401337-Apple", - "OperationCode": "MacAppDeveloperSign", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": { - "Hardening": "Enable" + --output="$(Build.ArtifactStagingDirectory)/installers/gcm-${{ dim.runtime }}-$(version).pkg" + - ${{ if eq(parameters.esrp, true) }}: + # ESRP code signing for macOS requires the files be packaged in a zip file first + - task: Bash@3 + displayName: 'Prepare installer package for signing' + inputs: + targetType: inline + script: | + mkdir -p $(Build.ArtifactStagingDirectory)/tosign + cd $(Build.ArtifactStagingDirectory)/installers + zip -rX $(Build.ArtifactStagingDirectory)/tosign/installers.zip *.pkg + - task: EsrpCodeSigning@5 + displayName: 'Sign installer package' + inputs: + connectedServiceName: '$(esrpAppConnectionName)' + useMSIAuthentication: true + appRegistrationClientId: '$(esrpClientId)' + appRegistrationTenantId: '$(esrpTenantId)' + authAkvName: '$(esrpKeyVaultName)' + authSignCertName: '$(esrpSignReqCertName)' + serviceEndpointUrl: '$(esrpEndpointUrl)' + folderPath: '$(Build.ArtifactStagingDirectory)/tosign' + pattern: 'installers.zip' + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-401337-Apple", + "OperationCode": "MacAppDeveloperSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "Hardening": "Enable" + } } - } - ] - # Extract signed installer, overwriting the unsigned installer - - task: Bash@3 - displayName: 'Extract signed installer package' - inputs: - targetType: inline - script: | - unzip -uo $(Build.ArtifactStagingDirectory)/tosign/installers-presign.zip -d $(Build.ArtifactStagingDirectory)/installers - - task: Bash@3 - displayName: 'Prepare installer package for notarization' - inputs: - targetType: inline - script: | - mkdir -p $(Build.ArtifactStagingDirectory)/tosign - cd $(Build.ArtifactStagingDirectory)/installers - zip -rX $(Build.ArtifactStagingDirectory)/tosign/installers.zip *.pkg - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Notarize installer package' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/tosign' - pattern: 'installers.zip' - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-401337-Apple", - "OperationCode": "MacAppNotarize", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": { - "BundleId": "com.microsoft.gitcredentialmanager" + ] + # Extract signed installer, overwriting the unsigned installer + - task: Bash@3 + displayName: 'Extract signed installer package' + inputs: + targetType: inline + script: | + unzip -uo $(Build.ArtifactStagingDirectory)/tosign/installers.zip -d $(Build.ArtifactStagingDirectory)/installers + - task: Bash@3 + displayName: 'Prepare installer package for notarization' + inputs: + targetType: inline + script: | + mkdir -p $(Build.ArtifactStagingDirectory)/tosign + cd $(Build.ArtifactStagingDirectory)/installers + # Remove previous installers.zip to avoid any confusion + rm -f $(Build.ArtifactStagingDirectory)/tosign/installers.zip + zip -rX $(Build.ArtifactStagingDirectory)/tosign/installers.zip *.pkg + - task: EsrpCodeSigning@5 + displayName: 'Notarize installer package' + inputs: + connectedServiceName: '$(esrpAppConnectionName)' + useMSIAuthentication: true + appRegistrationClientId: '$(esrpClientId)' + appRegistrationTenantId: '$(esrpTenantId)' + authAkvName: '$(esrpKeyVaultName)' + authSignCertName: '$(esrpSignReqCertName)' + serviceEndpointUrl: '$(esrpEndpointUrl)' + folderPath: '$(Build.ArtifactStagingDirectory)/tosign' + pattern: 'installers.zip' + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-401337-Apple", + "OperationCode": "MacAppNotarize", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "BundleId": "com.microsoft.gitcredentialmanager" + } } - } - ] - # Extract signed and notarized installer pkg files, overwriting the unsigned files, ready for upload - - task: Bash@3 - displayName: 'Extract signed and notarized installer package' - inputs: - targetType: inline - script: | - unzip -uo $(Build.ArtifactStagingDirectory)/tosign/installers.zip -d $(Build.ArtifactStagingDirectory)/installers + ] + # Extract signed and notarized installer pkg files, overwriting the unsigned files, ready for upload + - task: Bash@3 + displayName: 'Extract signed and notarized installer package' + inputs: + targetType: inline + script: | + unzip -uo $(Build.ArtifactStagingDirectory)/tosign/installers.zip -d $(Build.ArtifactStagingDirectory)/installers - task: ArchiveFiles@2 - displayName: 'Archive signed payload' + displayName: 'Archive payload' inputs: rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/payload' includeRootFolder: false @@ -595,32 +603,32 @@ extends: mkdir -p $(Build.ArtifactStagingDirectory)/installers mv $(Build.ArtifactStagingDirectory)/pkg/tar/*.tar.gz $(Build.ArtifactStagingDirectory)/installers mv $(Build.ArtifactStagingDirectory)/pkg/deb/*.deb $(Build.ArtifactStagingDirectory)/installers - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign Debian package' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/installers' - pattern: | - **/*.deb - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-453387-Pgp", - "OperationCode": "LinuxSign", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": {} - } - ] + - ${{ if eq(parameters.esrp, true) }}: + - task: EsrpCodeSigning@5 + displayName: 'Sign Debian package' + inputs: + connectedServiceName: '$(esrpAppConnectionName)' + useMSIAuthentication: true + appRegistrationClientId: '$(esrpClientId)' + appRegistrationTenantId: '$(esrpTenantId)' + authAkvName: '$(esrpKeyVaultName)' + authSignCertName: '$(esrpSignReqCertName)' + serviceEndpointUrl: '$(esrpEndpointUrl)' + folderPath: '$(Build.ArtifactStagingDirectory)/installers' + pattern: | + **/*.deb + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-453387-Pgp", + "OperationCode": "LinuxSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": {} + } + ] - task: Bash@3 displayName: 'Collect artifacts for publishing' inputs: @@ -672,46 +680,47 @@ extends: arguments: | -Configuration Release ` -Output "$(Build.ArtifactStagingDirectory)/nupkg" - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign payload' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/nupkg' - pattern: | - **/*.exe - **/*.dll - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-230012", - "OperationCode": "SigntoolSign", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": { - "OpusName": "Microsoft", - "OpusInfo": "https://www.microsoft.com", - "FileDigest": "/fd SHA256", - "PageHash": "/NPH", - "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + - ${{ if eq(parameters.esrp, true) }}: + - task: EsrpCodeSigning@5 + condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) + displayName: 'Sign payload' + inputs: + connectedServiceName: '$(esrpAppConnectionName)' + useMSIAuthentication: true + appRegistrationClientId: '$(esrpClientId)' + appRegistrationTenantId: '$(esrpTenantId)' + authAkvName: '$(esrpKeyVaultName)' + authSignCertName: '$(esrpSignReqCertName)' + serviceEndpointUrl: '$(esrpEndpointUrl)' + folderPath: '$(Build.ArtifactStagingDirectory)/nupkg' + pattern: | + **/*.exe + **/*.dll + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "https://www.microsoft.com", + "FileDigest": "/fd SHA256", + "PageHash": "/NPH", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + }, + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolVerify", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": {} } - }, - { - "KeyCode": "CP-230012", - "OperationCode": "SigntoolVerify", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": {} - } - ] + ] - task: PowerShell@2 displayName: 'Create NuGet packages' inputs: @@ -722,33 +731,34 @@ extends: -Version "$(version)" ` -PackageRoot "$(Build.ArtifactStagingDirectory)/nupkg" ` -Output "$(Build.ArtifactStagingDirectory)/packages" - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign NuGet packages' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/packages' - pattern: | - **/*.nupkg - **/*.snupkg - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-401405", - "OperationCode": "NuGetSign", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": {} - } - ] + - ${{ if eq(parameters.esrp, true) }}: + - task: EsrpCodeSigning@5 + condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) + displayName: 'Sign NuGet packages' + inputs: + connectedServiceName: '$(esrpAppConnectionName)' + useMSIAuthentication: true + appRegistrationClientId: '$(esrpClientId)' + appRegistrationTenantId: '$(esrpTenantId)' + authAkvName: '$(esrpKeyVaultName)' + authSignCertName: '$(esrpSignReqCertName)' + serviceEndpointUrl: '$(esrpEndpointUrl)' + folderPath: '$(Build.ArtifactStagingDirectory)/packages' + pattern: | + **/*.nupkg + **/*.snupkg + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": {} + } + ] - stage: release displayName: 'Release' diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 7bb45f26a..94f275915 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -51,7 +51,7 @@ jobs: shell: bash run: | mkdir -p artifacts/bin - mv out/windows/Installer.Windows/bin/Release/net472/${{ matrix.runtime }}/gcm*.exe artifacts/ + mv out/windows/Installer.Windows/bin/Release/net472/gcm*.exe artifacts/ mv out/windows/Installer.Windows/bin/Release/net472/${{ matrix.runtime }} artifacts/bin/ cp out/windows/Installer.Windows/bin/Release/net472/${{ matrix.runtime }}.sym/* artifacts/bin/${{ matrix.runtime }}/ diff --git a/VERSION b/VERSION index 21b5059f4..ba4940bb1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.7.0.0 +2.7.1.0 diff --git a/docs/enterprise-config.md b/docs/enterprise-config.md index 97544a33f..d0cf30aba 100644 --- a/docs/enterprise-config.md +++ b/docs/enterprise-config.md @@ -88,7 +88,36 @@ $ defaults read git-credential-manager configuration ## Linux -Default configuration setting stores has not been implemented. +Default settings values come from the `/etc/git-credential-manager/config.d` +directory. Each file in this directory represents a single settings dictionary. + +All files in this directory are read at runtime and merged into a single +collection of settings, in the order they are read from the file system. +To provide a stable ordering, it is recommended to prefix each filename with a +number, e.g. `42-my-settings`. + +The format of each file is a simple set of key/value pairs, separated by an +`=` sign, and each line separated by a line-feed (`\n`, LF) character. +Comments are identified by a `#` character at the beginning of a line. + +For example: + +```text +# /etc/git-credential-manager/config.d/00-example1 +credential.noguiprompt=0 +``` + +```text +# /etc/git-credential-manager/config.d/01-example2 +credential.trace=true +credential.traceMsAuth=true +``` + +All settings names and values are the same as the [Git configuration][config] +reference. + +> Note: These files are read once at startup. If changes are made to these files +they will not be reflected in an already running process. [environment]: environment.md [config]: configuration.md diff --git a/src/linux/Packaging.Linux/pack.sh b/src/linux/Packaging.Linux/pack.sh index 821d66ea1..2b3999267 100755 --- a/src/linux/Packaging.Linux/pack.sh +++ b/src/linux/Packaging.Linux/pack.sh @@ -84,12 +84,12 @@ if test -z "$RUNTIME"; then fi TAROUT="$OUTPUT_ROOT/tar" -TARBALL="$TAROUT/gcm-$RUNTIME.$VERSION.tar.gz" -SYMTARBALL="$TAROUT/gcm-$RUNTIME.$VERSION-symbols.tar.gz" +TARBALL="$TAROUT/gcm-$RUNTIME-$VERSION.tar.gz" +SYMTARBALL="$TAROUT/gcm-$RUNTIME-$VERSION-symbols.tar.gz" DEBOUT="$OUTPUT_ROOT/deb" DEBROOT="$DEBOUT/root" -DEBPKG="$DEBOUT/gcm-$RUNTIME.$VERSION.deb" +DEBPKG="$DEBOUT/gcm-$RUNTIME-$VERSION.deb" mkdir -p "$DEBROOT" # Set full read, write, execute permissions for owner and just read and execute permissions for group and other diff --git a/src/shared/Core.Tests/Interop/Linux/LinuxConfigParserTests.cs b/src/shared/Core.Tests/Interop/Linux/LinuxConfigParserTests.cs new file mode 100644 index 000000000..b31a3be30 --- /dev/null +++ b/src/shared/Core.Tests/Interop/Linux/LinuxConfigParserTests.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using GitCredentialManager.Interop.Linux; +using GitCredentialManager.Tests.Objects; +using Xunit; + +namespace GitCredentialManager.Tests.Interop.Linux; + +public class LinuxConfigParserTests +{ + [Fact] + public void LinuxConfigParser_Parse() + { + const string contents = + """ + # + # This is a config file complete with comments + # and empty.. + + # lines, as well as lines with.. + # + # only whitespace (like above ^), and.. + invalid lines like this one, not a comment + # Here's the first real properties: + core.overrideMe=This is the first config value + baz.specialChars=I contain special chars like = in my value # this is a comment + # and let's have with a comment that also contains a = in side + # + core.overrideMe=This is the second config value + bar.scope.foo=123456 + core.overrideMe=This is the correct value + ###### comments that start ## with whitespace and extra ## inside + strings.one="here we have a dq string" + strings.two='here we have a sq string' + strings.three= 'here we have another sq string' # have another sq string + strings.four="this has 'nested quotes' inside" + strings.five='mixed "quotes" the other way around' + strings.six='this has an \'escaped\' set of quotes' + """; + + var expected = new Dictionary + { + ["core.overrideMe"] = "This is the correct value", + ["bar.scope.foo"] = "123456", + ["baz.specialChars"] = "I contain special chars like = in my value", + ["strings.one"] = "here we have a dq string", + ["strings.two"] = "here we have a sq string", + ["strings.three"] = "here we have another sq string", + ["strings.four"] = "this has 'nested quotes' inside", + ["strings.five"] = "mixed \"quotes\" the other way around", + ["strings.six"] = "this has an \\'escaped\\' set of quotes", + }; + + var parser = new LinuxConfigParser(new NullTrace()); + + Assert.Equal(expected, parser.Parse(contents)); + } +} \ No newline at end of file diff --git a/src/shared/Core.Tests/Interop/Linux/LinuxSettingsTests.cs b/src/shared/Core.Tests/Interop/Linux/LinuxSettingsTests.cs new file mode 100644 index 000000000..7b9d7e893 --- /dev/null +++ b/src/shared/Core.Tests/Interop/Linux/LinuxSettingsTests.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using GitCredentialManager.Interop.Linux; +using GitCredentialManager.Tests.Objects; +using Xunit; + +namespace GitCredentialManager.Tests.Interop.Linux; + +public class LinuxSettingsTests +{ + [LinuxFact] + public void LinuxSettings_TryGetExternalDefault_CombinesFiles() + { + var env = new TestEnvironment(); + var git = new TestGit(); + var trace = new NullTrace(); + var fs = new TestFileSystem(); + + var utf8 = EncodingEx.UTF8NoBom; + + fs.Directories = new HashSet + { + "/", + "/etc", + "/etc/git-credential-manager", + "/etc/git-credential-manager/config.d" + }; + + const string config1 = "core.overrideMe=value1"; + const string config2 = "core.overrideMe=value2"; + const string config3 = "core.overrideMe=value3"; + + fs.Files = new Dictionary + { + ["/etc/git-credential-manager/config.d/01-first"] = utf8.GetBytes(config1), + ["/etc/git-credential-manager/config.d/02-second"] = utf8.GetBytes(config2), + ["/etc/git-credential-manager/config.d/03-third"] = utf8.GetBytes(config3), + }; + + var settings = new LinuxSettings(env, git, trace, fs); + + bool result = settings.TryGetExternalDefault( + "core", null, "overrideMe", out string value); + + Assert.True(result); + Assert.Equal("value3", value); + } +} diff --git a/src/shared/Core/CommandContext.cs b/src/shared/Core/CommandContext.cs index d3ef1dbf6..b5bade471 100644 --- a/src/shared/Core/CommandContext.cs +++ b/src/shared/Core/CommandContext.cs @@ -148,7 +148,7 @@ public CommandContext() gitPath, FileSystem.GetCurrentDirectory() ); - Settings = new Settings(Environment, Git); + Settings = new LinuxSettings(Environment, Git, Trace, FileSystem); } else { diff --git a/src/shared/Core/Constants.cs b/src/shared/Core/Constants.cs index 4777b0cf8..93346984b 100644 --- a/src/shared/Core/Constants.cs +++ b/src/shared/Core/Constants.cs @@ -15,6 +15,7 @@ public static class Constants public const string AuthorityIdAuto = "auto"; public const string GcmDataDirectoryName = ".gcm"; + public const string LinuxAppDefaultsDirectoryPath = "/etc/git-credential-manager/config.d"; public const string MacOSBundleId = "git-credential-manager"; public static readonly Guid DevBoxPartnerId = new("e3171dd9-9a5f-e5be-b36c-cc7c4f3f3bcf"); diff --git a/src/shared/Core/Interop/Linux/LinuxConfigParser.cs b/src/shared/Core/Interop/Linux/LinuxConfigParser.cs new file mode 100644 index 000000000..1caa918fc --- /dev/null +++ b/src/shared/Core/Interop/Linux/LinuxConfigParser.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace GitCredentialManager.Interop.Linux; + +public class LinuxConfigParser +{ +#if NETFRAMEWORK + private const string SQ = "'"; + private const string DQ = "\""; + private const string Hash = "#"; +#else + private const char SQ = '\''; + private const char DQ = '"'; + private const char Hash = '#'; +#endif + + private static readonly Regex LineRegex = new(@"^\s*(?[a-zA-Z0-9\.-]+)\s*=\s*(?.+?)\s*(?:#.*)?$"); + + private readonly ITrace _trace; + + public LinuxConfigParser(ITrace trace) + { + EnsureArgument.NotNull(trace, nameof(trace)); + + _trace = trace; + } + + public IDictionary Parse(string content) + { + var result = new Dictionary(GitConfigurationKeyComparer.Instance); + + IEnumerable lines = content.Split(['\n'], StringSplitOptions.RemoveEmptyEntries); + + foreach (string line in lines) + { + // Ignore empty lines or full-line comments + var trimmedLine = line.Trim(); + if (string.IsNullOrEmpty(trimmedLine) || trimmedLine.StartsWith(Hash)) + continue; + + var match = LineRegex.Match(trimmedLine); + if (!match.Success) + { + _trace.WriteLine($"Invalid config line format: {line}"); + continue; + } + + string key = match.Groups["key"].Value; + string value = match.Groups["value"].Value; + + // Remove enclosing quotes from the value, if any + if ((value.StartsWith(DQ) && value.EndsWith(DQ)) || (value.StartsWith(SQ) && value.EndsWith(SQ))) + value = value.Substring(1, value.Length - 2); + + result[key] = value; + } + + return result; + } +} diff --git a/src/shared/Core/Interop/Linux/LinuxSettings.cs b/src/shared/Core/Interop/Linux/LinuxSettings.cs new file mode 100644 index 000000000..af4c578a8 --- /dev/null +++ b/src/shared/Core/Interop/Linux/LinuxSettings.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Avalonia.Markup.Xaml.MarkupExtensions; + +namespace GitCredentialManager.Interop.Linux; + +public class LinuxSettings : Settings +{ + private readonly ITrace _trace; + private readonly IFileSystem _fs; + + private IDictionary _extConfigCache; + + /// + /// Reads settings from Git configuration, environment variables, and defaults from the + /// /etc/git-credential-manager.d app configuration directory. + /// + public LinuxSettings(IEnvironment environment, IGit git, ITrace trace, IFileSystem fs) + : base(environment, git) + { + EnsureArgument.NotNull(trace, nameof(trace)); + EnsureArgument.NotNull(fs, nameof(fs)); + + _trace = trace; + _fs = fs; + + PlatformUtils.EnsureLinux(); + } + + protected internal override bool TryGetExternalDefault(string section, string scope, string property, out string value) + { + value = null; + + _extConfigCache ??= ReadExternalConfiguration(); + + if (_extConfigCache is null) + return false; // No external config found (or failed to read) + + string name = string.IsNullOrWhiteSpace(scope) + ? $"{section}.{property}" + : $"{section}.{scope}.{property}"; + + // Check if the setting exists in the configuration + if (!_extConfigCache.TryGetValue(name, out value)) + { + // No property exists + return false; + } + + _trace.WriteLine($"Default setting found in app configuration directory: {name}={value}"); + return true; + } + + private IDictionary ReadExternalConfiguration() + { + try + { + // Check for system-wide config files in /etc/git-credential-manager/config.d and concatenate them together + // in alphabetical order to form a single configuration. + const string configDir = Constants.LinuxAppDefaultsDirectoryPath; + if (!_fs.DirectoryExists(configDir)) + { + // No configuration directory exists + return null; + } + + // Get all the files in the configuration directory + IEnumerable files = _fs.EnumerateFiles(configDir, "*"); + + // Read the contents of each file and concatenate them together + var combinedFile = new StringBuilder(); + foreach (string file in files) + { + using Stream stream = _fs.OpenFileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); + using var reader = new StreamReader(stream); + string contents = reader.ReadToEnd(); + combinedFile.Append(contents); + combinedFile.Append('\n'); + } + + var parser = new LinuxConfigParser(_trace); + + return parser.Parse(combinedFile.ToString()); + } + catch (Exception ex) + { + // Reading defaults is not critical to the operation of the application + // so we can ignore any errors and just log the failure. + _trace.WriteLine("Failed to read default setting from app configuration directory."); + _trace.WriteException(ex); + return null; + } + } +} diff --git a/src/shared/Core/Interop/MacOS/MacOSSettings.cs b/src/shared/Core/Interop/MacOS/MacOSSettings.cs index 3ef2c8247..9b7677a72 100644 --- a/src/shared/Core/Interop/MacOS/MacOSSettings.cs +++ b/src/shared/Core/Interop/MacOS/MacOSSettings.cs @@ -19,7 +19,7 @@ public MacOSSettings(IEnvironment environment, IGit git, ITrace trace) PlatformUtils.EnsureMacOS(); } - protected override bool TryGetExternalDefault(string section, string scope, string property, out string value) + protected internal override bool TryGetExternalDefault(string section, string scope, string property, out string value) { value = null; diff --git a/src/shared/Core/Interop/Windows/WindowsSettings.cs b/src/shared/Core/Interop/Windows/WindowsSettings.cs index abdd9ee0e..888e9aa7d 100644 --- a/src/shared/Core/Interop/Windows/WindowsSettings.cs +++ b/src/shared/Core/Interop/Windows/WindowsSettings.cs @@ -17,7 +17,7 @@ public WindowsSettings(IEnvironment environment, IGit git, ITrace trace) PlatformUtils.EnsureWindows(); } - protected override bool TryGetExternalDefault(string section, string scope, string property, out string value) + protected internal override bool TryGetExternalDefault(string section, string scope, string property, out string value) { value = null; diff --git a/src/shared/Core/Settings.cs b/src/shared/Core/Settings.cs index 0e24ce9a3..af3dcf99c 100644 --- a/src/shared/Core/Settings.cs +++ b/src/shared/Core/Settings.cs @@ -489,7 +489,7 @@ public IEnumerable GetSettingValues(string envarName, string section, st /// Configuration property name. /// Value of the configuration setting, or null. /// True if a default setting has been set, false otherwise. - protected virtual bool TryGetExternalDefault(string section, string scope, string property, out string value) + protected internal virtual bool TryGetExternalDefault(string section, string scope, string property, out string value) { value = null; return false; diff --git a/src/windows/Installer.Windows/Installer.Windows.csproj b/src/windows/Installer.Windows/Installer.Windows.csproj index eae3631f0..ec678fe5f 100644 --- a/src/windows/Installer.Windows/Installer.Windows.csproj +++ b/src/windows/Installer.Windows/Installer.Windows.csproj @@ -13,8 +13,11 @@ net472 false false - $(PlatformOutPath)Installer.Windows\bin\$(Configuration)\net472\$(RuntimeIdentifier) + $(PlatformOutPath)Installer.Windows\bin\$(Configuration)\net472\$(RuntimeIdentifier)\ 6.3.1 + + false diff --git a/src/windows/Installer.Windows/layout.ps1 b/src/windows/Installer.Windows/layout.ps1 index 3fc43ab36..3b1624896 100644 --- a/src/windows/Installer.Windows/layout.ps1 +++ b/src/windows/Installer.Windows/layout.ps1 @@ -1,6 +1,10 @@ # Inputs param ([Parameter(Mandatory)] $Configuration, [Parameter(Mandatory)] $Output, $RuntimeIdentifier, $SymbolOutput) +# Trim trailing slashes from output paths +$Output = $Output.TrimEnd('\','/') +$SymbolOutput = $SymbolOutput.TrimEnd('\','/') + Write-Output "Output: $Output" # Determine a runtime if one was not provided