Providers/FreeIPA/Public/New-FreeIPACertificate.ps1
|
function New-FreeIPACertificate { <# .EXTERNALHELP TestEnvironment-Help.xml .SYNOPSIS Has the realm's CA issue the seeded certificates from Data\FreeIPACertificates.csv, and revokes the ones the data says are revoked #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [OutputType([PSCustomObject])] param( [Parameter()] [string[]]$CertificateKey, [Parameter()] [switch]$PassThru ) $connection = Get-FreeIPAConnection $marker = Get-FreeIPASeedMarker -Connection $connection $csvPath = Join-Path -Path (Get-FreeIPADataPath) -ChildPath 'FreeIPACertificates.csv' $rows = @(Import-Csv -Path $csvPath -Encoding UTF8) if ($CertificateKey) { $rows = @($rows | Where-Object { $CertificateKey -contains $_.Key }) $unknown = @($CertificateKey | Where-Object { $rows.Key -notcontains $_ }) if ($unknown) { throw "No definition in $csvPath for: $($unknown -join ', ')" } } $result = [PSCustomObject]@{ TotalCertificates = $rows.Count Issued = 0 Revoked = 0 Existing = 0 Certificates = @() Errors = @() } if ([string]::IsNullOrWhiteSpace($connection.Realm)) { throw 'The connection carries no realm, so a certificate subject cannot be built.' } $first = { param($value) if ($value -is [array]) { if ($value.Count -gt 0) { [string]$value[0] } else { '' } } else { if ($null -eq $value) { '' } else { [string]$value } } } $hostNamed = { param($key) Resolve-FreeIPASeedName -Key $key -Kind Host -Marker $marker -Connection $connection } # What the CA is asked for depends on the kind: the login or the host name as the common # name, the realm as the organisation, and the principal the realm checks it against. $describe = { param($row) switch ($row.Kind) { 'User' { $uid = $row.Principal @{ Principal = ('{0}@{1}' -f $uid, $connection.Realm); CommonName = $uid; Search = @{ user = @($uid) }; Owner = $uid; HostName = $null } } 'Service' { $type, $hostKey = $row.Principal -split '/', 2 $fqdn = & $hostNamed $hostKey $principal = '{0}/{1}@{2}' -f $type, $fqdn, $connection.Realm @{ Principal = $principal; CommonName = $fqdn; Search = @{ service = @($principal) }; Owner = $principal; HostName = $fqdn } } 'Host' { $fqdn = & $hostNamed $row.Principal @{ Principal = ('host/{0}@{1}' -f $fqdn, $connection.Realm); CommonName = $fqdn; Search = @{ host = @($fqdn) }; Owner = $fqdn; HostName = $fqdn } } default { throw "Unknown certificate kind '$($row.Kind)' on row '$($row.Key)'." } } } $certificates = [System.Collections.Generic.List[object]]::new() foreach ($row in $rows) { $wanted = if ($row.State -eq 'Revoked') { 'REVOKED' } else { 'VALID' } try { $target = & $describe $row } catch { $result.Errors += $_.Exception.Message Write-Error $_.Exception.Message continue } if (-not $PSCmdlet.ShouldProcess("$($target.Principal) ($($row.State))", 'Issue FreeIPA certificate')) { continue } try { if ($row.Profile -notlike 'builtin:*') { throw "A certificate profile can only be referenced as builtin:<name>; got '$($row.Profile)'." } $profileId = $row.Profile.Substring(8) # Already there in the state the row asks for: nothing to ask the CA. $searchOptions = @{ all = $true } foreach ($key in $target.Search.Keys) { $searchOptions[$key] = [object[]]$target.Search[$key] } $held = @(Invoke-FreeIPARequest -Method 'cert_find' -Options $searchOptions -Find -Connection $connection) $match = @($held | Where-Object { (& $first $_.status) -eq $wanted } | Select-Object -First 1) if ($match.Count -gt 0) { $result.Existing++ $certificates.Add([PSCustomObject]@{ Key = $row.Key; Principal = $target.Principal; Serial = (& $first $match[0].serial_number) Subject = (& $first $match[0].subject); State = $row.State NotAfter = (ConvertFrom-FreeIPACertificateDate -Value $match[0].valid_not_after) }) continue } $dnsNames = @() $emails = @() if ($row.SanDns -eq 'TRUE' -and $target.HostName) { $dnsNames = @($target.HostName) } if ($row.SanEmail -eq 'TRUE' -and $row.Kind -eq 'User') { # The realm checks an email in the request against the user's own. $shown = Invoke-FreeIPARequest -Method 'user_show' -Arguments $row.Principal -Connection $connection $mail = & $first $(if ($shown.result.PSObject.Properties['mail']) { $shown.result.mail } else { $null }) if ($mail) { $emails = @($mail) } } $csr = New-FreeIPACertificateRequest -Subject ('CN={0},O={1}' -f $target.CommonName, $connection.Realm) -DnsName $dnsNames -EmailAddress $emails $issued = Invoke-FreeIPARequest -Method 'cert_request' -Arguments $csr -Connection $connection -Options @{ principal = $target.Principal profile_id = $profileId add = $true } $result.Issued++ $serial = & $first $issued.result.serial_number Write-Verbose "Issued certificate $serial to $($target.Principal)" if ($wanted -eq 'REVOKED') { $reason = if ($row.RevocationReason -match '^\d+$') { [int]$row.RevocationReason } else { 0 } $null = Invoke-FreeIPARequest -Method 'cert_revoke' -Arguments $serial -Options @{ revocation_reason = $reason } -Connection $connection $result.Revoked++ Write-Verbose "Revoked certificate $serial (reason $reason)" } $certificates.Add([PSCustomObject]@{ Key = $row.Key; Principal = $target.Principal; Serial = $serial Subject = (& $first $issued.result.subject); State = $row.State NotAfter = (ConvertFrom-FreeIPACertificateDate -Value $issued.result.valid_not_after) }) } catch { $message = "Failed to issue certificate '$($row.Key)' to $($target.Principal): $($_.Exception.Message)" $result.Errors += $message Write-Error $message } } $result.Certificates = $certificates.ToArray() Write-Verbose "Certificates: $($result.Issued) issued, $($result.Revoked) revoked, $($result.Existing) already there, $($result.Errors.Count) problems" if ($PassThru) { return $result } } |