SignertKryptertBundle
)Dette scriptet lager en SignertKryptertBundle
ved hjelp av PowerShell 7+. Det utfører komprimering med GZip, kryptering med AES-GCM og signering med X.509-sertifikater.
.\LagSignertKryptertBundle.ps1 `
-DatafilPath "C:\Sti\Til\MeldingsInnhold.json" `
-OutputFilePath "C:\Sti\Til\Output\SignertKryptertBundle.json" `
-ReceiverThumbprint "ABCDEF1234567890ABCDEF1234567890ABCDEF12" `
-SenderThumbprint "1234567890ABCDEF1234567890ABCDEF12345678" `
-SenderOrganizationIdentifier "123456789" `
-RapporteringFra "2024-01-01T00:00:00Z" `
-RapporteringTil "2024-01-31T23:59:59Z"
Scriptet utfører følgende oppgaver:
# Krever PowerShell 7+
param(
[Parameter(Mandatory=$true)]
[string]$DatafilPath, # sti til datafil med melding
[Parameter(Mandatory=$true)]
[string]$OutputFilePath, # sti til fil for lagring av SignertKryptertBundle
[Parameter(Mandatory=$true)]
[string]$ReceiverThumbprint, # LMR sitt sertifikat (offentlig nøkkel brukes til å kryptere meldingen)
[Parameter(Mandatory=$true)]
[string]$SenderThumbprint, # Bærum sitt sertifikat (brukes til å signere)
[Parameter(Mandatory=$true)]
[string]$SenderOrganizationIdentifier, # Organisasjonsidentifikator for avsender
[Parameter(Mandatory=$true)]
[DateTime]$RapporteringFra,
[Parameter(Mandatory=$true)]
[DateTime]$RapporteringTil
)
# 1. Les innholdet i datafilen
if (-not (Test-Path $DatafilPath)) {
throw "Kunne ikke finne filen på $DatafilPath"
}
$fileContent = Get-Content $DatafilPath -Raw
# 2. Komprimer datafilen (GZip)
function Compress-StringToGZip {
param([string]$content)
$bytes = [System.Text.Encoding]::UTF8.GetBytes($content)
$outputStream = New-Object System.IO.MemoryStream
# Bruk CompressionLevel Optimal = 0
$gzip = New-Object System.IO.Compression.GZipStream($outputStream, [System.IO.Compression.CompressionLevel]::Optimal)
$gzip.Write($bytes, 0, $bytes.Length)
$gzip.Close()
$compressed = $outputStream.ToArray()
$outputStream.Dispose()
return $compressed
}
$compressedContent = Compress-StringToGZip $fileContent
# 3. Generer tilfeldig AES-nøkkel (32 bytes) og nonce (12 bytes)
$key = New-Object byte[] 32
$nonce = New-Object byte[] 12
[System.Security.Cryptography.RandomNumberGenerator]::Fill($key)
[System.Security.Cryptography.RandomNumberGenerator]::Fill($nonce)
# 4. Krypter med AES-GCM
$encryptedContent = New-Object byte[] $compressedContent.Length
$authTag = New-Object byte[] 16 # 128-bit autentiseringsmerke
$aesGcm = [System.Security.Cryptography.AesGcm]::new($key)
$aesGcm.Encrypt($nonce, $compressedContent, $encryptedContent, $authTag)
$aesGcm.Dispose()
# 5. Krypter AES-nøkkelen med mottakerens **offentlige** RSA-nøkkel
function Get-CertificateByThumbprint([string]$thumbprint) {
# Juster lagring hvis nødvendig: LocalMachine vs CurrentUser, My vs Root, osv.
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","CurrentUser")
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly)
$cert = $store.Certificates | Where-Object { $_.Thumbprint -eq $thumbprint.ToUpper() }
$store.Close()
return $cert
}
$receiverCert = Get-CertificateByThumbprint $ReceiverThumbprint
if (-not $receiverCert) {
throw "Mottakersertifikat ble ikke funnet i LocalMachine\My med thumbprint $ReceiverThumbprint"
}
$receiverPublicKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPublicKey($receiverCert)
if (-not $receiverPublicKey) {
throw "Kunne ikke hente RSA offentlig nøkkel fra mottakersertifikat."
}
$encryptedKey = $receiverPublicKey.Encrypt($key, [System.Security.Cryptography.RSAEncryptionPadding]::OaepSHA256)
# 6. Signer det krypterte innholdet med avsenderens **private** RSA-nøkkel
$senderCert = Get-CertificateByThumbprint $SenderThumbprint
if (-not $senderCert) {
throw "Avsendersertifikat ble ikke funnet i LocalMachine\My med thumbprint $SenderThumbprint"
}
$senderPrivateKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($senderCert)
if (-not $senderPrivateKey) {
throw "Kunne ikke hente RSA privat nøkkel fra avsendersertifikat."
}
$messageHash = [System.Security.Cryptography.SHA256]::HashData($encryptedContent)
$signature = $senderPrivateKey.SignHash(
$messageHash,
[System.Security.Cryptography.HashAlgorithmName]::SHA256,
[System.Security.Cryptography.RSASignaturePadding]::Pkcs1
)
# 7. Bygg samme JSON-nyttelast som C# SignertKryptertBundle
$signertKryptertBundle = [PSCustomObject]@{
EncryptedContent = [System.Convert]::ToBase64String($encryptedContent)
RapporteringFra = $RapporteringFra
RapporteringTil = $RapporteringTil
EncryptionCertificateThumbprint= $ReceiverThumbprint
EncryptedKey = [System.Convert]::ToBase64String($encryptedKey)
MessageFormatVersion = "1.0"
Nonce = [System.Convert]::ToBase64String($nonce)
AuthenticationTag = [System.Convert]::ToBase64String($authTag)
MessageId = [System.Guid]::NewGuid().ToString()
GeneratedAt = [DateTime]::UtcNow.ToString("o")
SenderOrganizationIdentifier = $SenderOrganizationIdentifier
SignatureCertificateThumbprint = $SenderThumbprint
Signature = [System.Convert]::ToBase64String($signature)
}
# 8. Lagre SignertKryptertBundle til fil
$signertKryptertBundle | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputFilePath
Write-Host "SignertKryptertBundle er lagret til $OutputFilePath"
SignertKryptertBundle
til LegemiddelregisteretDette eksempelet viser hvordan du kan sende den krypterte bundlen til Legemiddelregisteret via API ved hjelp av PowerShell-scripter som håndterer autentisering og opplasting. Løsningen bruker DPoP (Demonstrating Proof-of-Possession) tokens for å autentisere mot HelseID og sikre kommunikasjonen med Legemiddelregisterets API.
Scriptsystemet består av følgende filer:
upload.ps1
: Hovedscript for å laste opp filenClientCredentials.ps1
: Håndterer autentisering mot HelseID ved bruk av DPoPGenerateClientAssertion.ps1
: Lager JWT-token for klientautentiseringGenerateDPoPToken.ps1
: Genererer DPoP-tokens for sikker API-kommunikasjonconfig.json
: Konfigurasjonsfil med endepunkter og klientinformasjonconfig.json
fil med nødvendig konfigurasjon:{
"FhirMottakEndpoint": "https://api.legemiddelregisteret.no/mottak/",
"ClientCredentials": {
"clientId": "ditt-klient-id",
"issuer": "https://helseid-sts.test.nhn.no",
"scope": "fhi:lmr.fhirmottak/api",
"tokenEndpoint": "https://helseid-sts.test.nhn.no/connect/token",
"privateKey": {
"kty": "RSA",
"kid": "din-kid-verdi",
"n": "base64url-encoded-modulus",
"e": "AQAB",
"d": "base64url-encoded-d-value",
"p": "base64url-encoded-p-value",
"q": "base64url-encoded-q-value",
"dp": "base64url-encoded-dp-value",
"dq": "base64url-encoded-dq-value",
"qi": "base64url-encoded-qi-value"
}
}
}
upload.ps1
for å laste opp den genererte SignertKryptertBundle
:.\upload.ps1 -FilePath "C:\Sti\Til\Output\SignertKryptertBundle.json"
param(
[Parameter(Mandatory=$true)]
[string]$FilePath
)
# Verifiser at filen eksisterer
if (-not (Test-Path $FilePath)) {
Write-Host "Filen '$FilePath' ble ikke funnet." -ForegroundColor Red
exit 1
}
$configPath = "$PSScriptRoot\config.json"
$configContent = Get-Content -Path $configPath -Raw
$config = $configContent | ConvertFrom-Json
$FhirMottakEndpoint = $config.FhirMottakEndpoint
try {
# Les JSON-filen
$jsonContent = Get-Content -Path $FilePath -Raw
# Valider at innholdet er gyldig JSON
$null = $jsonContent | ConvertFrom-Json
# Generate headers with dpop-proof and dpop-token issued by HelseId
$headers = . "$PSScriptRoot\ClientCredentials.ps1"
try {
$response = Invoke-RestMethod -Uri $FhirMottakEndpoint -Method Post -ContentType "application/json" -Body $jsonContent -Headers $headers
Write-Host "Sending av melding var vellykket!" -ForegroundColor Green
# Returner responsen for videre behandling om nødvendig
return $response
}
catch {
Write-Host "Failed to POST: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
}
catch {
Write-Host "Feil ved lesing eller validering av JSON-fil: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
. "$PSScriptRoot\GenerateClientAssertion.ps1"
. "$PSScriptRoot\GenerateDPoPToken.ps1"
$configPath = "$PSScriptRoot\config.json"
$configContent = Get-Content -Path $configPath -Raw
$config = $configContent | ConvertFrom-Json
$privateKeyObject = $config.ClientCredentials.privateKey
$privateKeyJson = $privateKeyObject | ConvertTo-Json -Compress
$clientId = $config.ClientCredentials.clientId
$issuer = $config.ClientCredentials.issuer
$scope = $config.ClientCredentials.scope
$tokenEndpoint = $config.ClientCredentials.tokenEndpoint
$FhirMottakEndpoint = $config.FhirMottakEndpoint
# Convert JSON to PowerShell object
$key = $privateKeyJson | ConvertFrom-Json
$clientAssertion = CreateClientAssertionFromPrivateKeyJson -clientId $clientId -audience $issuer -privateKey $key
# Generate dpop prood withtout nonce
$dpopProof = Generate-DPoPToken -HttpMethod "POST" -HttpUri $tokenEndpoint -PrivateKeyJson $privateKeyJson
$headers = @{
DPoP = $dpopProof
}
$params = "client_id=$clientId&client_assertion=$clientAssertion&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&grant_type=client_credentials&scope=$scope"
try {
$response = Invoke-RestMethod -Method Post -Uri $tokenEndpoint -Body $params -Headers $headers -ContentType "application/x-www-form-urlencoded"
}
catch {
# Capture the HTTP response from the exception
$httpResponse = $_.Exception.Response
if ($httpResponse -ne $null) {
# Read the DPoP-Nonce header from the response
$nonceValues = $httpResponse.Headers.GetValues("DPoP-Nonce")
$nonce = $nonceValues -join ", "
Write-Host "Received DPoP-Nonce: $nonce"
}
else {
Write-Host "No web response available: $($_.Exception.Message)"
}
}
# Include nonce received from HelseId in new dpop proof
$dpopProof = Generate-DPoPToken -HttpMethod "POST" -HttpUri $tokenEndpoint -PrivateKeyJson $privateKeyJson -Nonce $nonce
$headers = @{
DPoP = $dpopProof
}
$response = Invoke-RestMethod -Method Post -Uri $tokenEndpoint -Body $params -Headers $headers -ContentType "application/x-www-form-urlencoded"
Write-Host "Received dpop access token: $response"
$accessToken = $response.access_token
# Calculate ath value based on the newly received dpop access token
$accessTokenBytes = [System.Text.Encoding]::UTF8.GetBytes($accessToken.Trim())
$sha256 = [System.Security.Cryptography.SHA256]::Create()
$hashBytes = $sha256.ComputeHash($accessTokenBytes)
$Ath = [Convert]::ToBase64String($hashBytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
# Generate new dpop proof with the ath included to use when making a request to the api
$dpopProof = Generate-DPoPToken -HttpMethod "POST" -HttpUri $FhirMottakEndpoint -PrivateKeyJson $privateKeyJson -Ath $Ath
$headers = @{
Authorization = "DPoP $accessToken"
DPoP = $dpopProof
}
Write-Output $headers
# Create client assertion
function CreateClientAssertionFromPrivateKeyJson ($clientId, $audience, [object]$privateKey) {
# Check if privateKey is initialized
if ($null -eq $privateKey) {
Write-Host "Error: privateKey is null"
return null
}
# JWT Header
$header = @{
alg = "PS512"
typ = "client-authentication+jwt"
kid = $privateKey.kid
} | ConvertTo-Json -Compress
$headerEncoded = Base64UrlEncode([System.Text.Encoding]::UTF8.GetBytes($header))
# JWT Payload
$now = [System.DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$payload = @{
sub = $clientId
iss = $clientId
aud = $audience
exp = $now + 60
iat = $now
nbf = $now
jti = [guid]::NewGuid().ToString()
} | ConvertTo-Json -Compress
$payloadEncoded = Base64UrlEncode([System.Text.Encoding]::UTF8.GetBytes($payload))
# Combine header and payload
$unsignedToken = "$headerEncoded.$payloadEncoded"
$rsa = [System.Security.Cryptography.RSACng]::new()
$parameters = @{
Modulus = Base64UrlDecode($privateKey.n)
Exponent = Base64UrlDecode($privateKey.e)
D = Base64UrlDecode($privateKey.d)
P = Base64UrlDecode($privateKey.p)
Q = Base64UrlDecode($privateKey.q)
DP = Base64UrlDecode($privateKey.dp)
DQ = Base64UrlDecode($privateKey.dq)
InverseQ = Base64UrlDecode($privateKey.qi)
}
$rsa.ImportParameters($parameters)
$signature = $rsa.SignData(
[System.Text.Encoding]::UTF8.GetBytes($unsignedToken),
[System.Security.Cryptography.HashAlgorithmName]::SHA512,
[System.Security.Cryptography.RSASignaturePadding]::Pss
)
$signatureEncoded = Base64UrlEncode($signature)
return "$headerEncoded.$payloadEncoded.$signatureEncoded"
}
# Base64 URL Encode function
function Base64UrlEncode($bytes) {
return [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
}
# Base64 URL Decode function
function Base64UrlDecode($base64Url) {
$base64 = $base64Url -replace '-', '+' -replace '_', '/'
switch ($base64.Length % 4) {
2 { $base64 += '==' }
3 { $base64 += '=' }
}
return [Convert]::FromBase64String($base64)
}
function Generate-DPoPToken {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$HttpMethod,
[Parameter(Mandatory = $true)]
[string]$HttpUri,
[Parameter(Mandatory = $true)]
[string]$PrivateKeyJson,
[Parameter(Mandatory = $false)]
[string]$Nonce, # Optional parameter to be used after reciving one from the authorization server
[Parameter(Mandatory = $false)]
[string]$Ath # Optional ath claim to be used when making requests to the resource server (should be the Base64Url-encoded SHA256 hash of the access token)
)
# Helper: Base64Url encoding function
function Base64UrlEncode($bytes) {
return [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
}
# Helper: Base64Url decoding function
function Base64UrlDecode($base64Url) {
$base64 = $base64Url -replace '-', '+' -replace '_', '/'
switch ($base64.Length % 4) {
2 { $base64 += '==' }
3 { $base64 += '=' }
}
return [Convert]::FromBase64String($base64)
}
# Parse the provided private key JSON into an object.
$privateKey = $PrivateKeyJson | ConvertFrom-Json
# Build RSAParameters from the JWK
$parameters = @{
Modulus = Base64UrlDecode($privateKey.n)
Exponent = Base64UrlDecode($privateKey.e)
D = Base64UrlDecode($privateKey.d)
P = Base64UrlDecode($privateKey.p)
Q = Base64UrlDecode($privateKey.q)
DP = Base64UrlDecode($privateKey.dp)
DQ = Base64UrlDecode($privateKey.dq)
InverseQ = Base64UrlDecode($privateKey.qi)
}
# Build the public key JWK for inclusion in the header.
$publicKeyJwk = @{
kty = "RSA"
n = Base64UrlEncode($parameters.Modulus)
e = Base64UrlEncode($parameters.Exponent)
alg = "PS512"
}
# Create an RSA object and import the parameters.
$rsa = [System.Security.Cryptography.RSACng]::new()
$rsa.ImportParameters($parameters)
# Get current Unix time for the iat claim.
$iat = [System.DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
# Construct the JWT header with the required "jwk" claim.
$header = @{
alg = "PS512"
typ = "dpop+jwt"
jwk = $publicKeyJwk
} | ConvertTo-Json -Compress
$headerEncoded = Base64UrlEncode([System.Text.Encoding]::UTF8.GetBytes($header))
# Construct the JWT payload with required DPoP claims.
$payload = @{
htm = $HttpMethod # HTTP method (e.g., "POST")
htu = $HttpUri # HTTP target URI
iat = $iat # Issued at time (Unix timestamp)
jti = [guid]::NewGuid().ToString() # Unique identifier to prevent replay
}
# If a nonce is provided, include it in the payload.
if ($Nonce) {
$payload.nonce = $Nonce
}
# If an ath value is provided, include it.
if ($Ath) {
$payload.ath = $Ath
}
$payload = $payload | ConvertTo-Json -Compress
$payloadEncoded = Base64UrlEncode([System.Text.Encoding]::UTF8.GetBytes($payload))
# Combine header and payload to create the unsigned token.
$unsignedToken = "$headerEncoded.$payloadEncoded"
# Sign the unsigned token using the RSA private key.
$dataToSign = [System.Text.Encoding]::UTF8.GetBytes($unsignedToken)
$signature = $rsa.SignData(
$dataToSign,
[System.Security.Cryptography.HashAlgorithmName]::SHA512,
[System.Security.Cryptography.RSASignaturePadding]::Pss
)
$signatureEncoded = Base64UrlEncode($signature)
# Construct the final DPoP proof token.
$dpopToken = "$unsignedToken.$signatureEncoded"
return $dpopToken
}