Azure.Security.ConfidentialLedger 1.0.0-beta.3

The ID prefix of this package has been reserved for one of the owners of this package by NuGet.org. Prefix Reserved
This is a prerelease version of Azure.Security.ConfidentialLedger.
There is a newer version of this package available.
See the version list below for details.
dotnet add package Azure.Security.ConfidentialLedger --version 1.0.0-beta.3
NuGet\Install-Package Azure.Security.ConfidentialLedger -Version 1.0.0-beta.3
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Azure.Security.ConfidentialLedger" Version="1.0.0-beta.3" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Azure.Security.ConfidentialLedger --version 1.0.0-beta.3
#r "nuget: Azure.Security.ConfidentialLedger, 1.0.0-beta.3"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Azure.Security.ConfidentialLedger as a Cake Addin
#addin nuget:?package=Azure.Security.ConfidentialLedger&version=1.0.0-beta.3&prerelease

// Install Azure.Security.ConfidentialLedger as a Cake Tool
#tool nuget:?package=Azure.Security.ConfidentialLedger&version=1.0.0-beta.3&prerelease

Azure Confidential Ledger client library for .NET

Azure Confidential Ledger provides a service for logging to an immutable, tamper-proof ledger. As part of the Azure Confidential Computing portfolio, Azure Confidential Ledger runs in SGX enclaves. It is built on Microsoft Research's Confidential Consortium Framework.

Source code | Package (NuGet)

Getting started

This section should include everything a developer needs to do to install and create their first client connection very quickly.

Install the package

Install the Confidential Ledger client library for .NET with NuGet:

dotnet add package Azure.Security.ConfidentialLedger --prerelease

Prerequisites

  • An Azure subscription.
  • A running instance of Azure Confidential Ledger.
  • A registered user in the Confidential Ledger with Administrator privileges.

Authenticate the client

Using Azure Active Directory

This document demonstrates using DefaultAzureCredential to authenticate to the Confidential Ledger via Azure Active Directory. However, any of the credentials offered by the Azure.Identity will be accepted. See the Azure.Identity documentation for more information about other credentials.

Using a client certificate

As an alternative to Azure Active Directory, clients may choose to use a client certificate to authenticate via mutual TLS.

Create a client

DefaultAzureCredential will automatically handle most Azure SDK client scenarios. To get started, set environment variables for the AAD identity registered with your Confidential Ledger.

export AZURE_CLIENT_ID="generated app id"
export AZURE_CLIENT_SECRET="random password"
export AZURE_TENANT_ID="tenant id"

Then, DefaultAzureCredential will be able to authenticate the ConfidentialLedgerClient.

Constructing the client also requires your Confidential Ledger's URL and id, which you can get from the Azure CLI or the Azure Portal. When you have retrieved those values, please replace instances of "my-ledger-id" and "https://my-ledger-url.confidential-ledger.azure.com" in the examples below

Because Confidential Ledgers use self-signed certificates securely generated and stored in an SGX enclave, the certificate for each Confidential Ledger must first be retrieved from the Confidential Ledger Identity Service.

Uri identityServiceEndpoint = new("https://identity.confidential-ledger.core.azure.com") // The hostname from the identityServiceUri
var identityClient = new ConfidentialLedgerIdentityServiceClient(identityServiceEndpoint);

// Get the ledger's  TLS certificate for our ledger.
string ledgerId = "<the ledger id>"; // ex. "my-ledger" from "https://my-ledger.eastus.cloudapp.azure.com"
Response response = identityClient.GetLedgerIdentity(ledgerId);
X509Certificate2 ledgerTlsCert = ConfidentialLedgerIdentityServiceClient.ParseCertificate(response);

Now we can construct the ConfidentialLedgerClient with a transport configuration that trusts the ledgerTlsCert.

// Create a certificate chain rooted with our TLS cert.
X509Chain certificateChain = new();
certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
certificateChain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
certificateChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
certificateChain.ChainPolicy.VerificationTime = DateTime.Now;
certificateChain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 0);
certificateChain.ChainPolicy.ExtraStore.Add(ledgerTlsCert);

var f = certificateChain.Build(ledgerTlsCert);

// Define a validation function to ensure that the ledger certificate is trusted by the ledger identity TLS certificate.
bool CertValidationCheck(HttpRequestMessage httpRequestMessage, X509Certificate2 cert, X509Chain x509Chain, SslPolicyErrors sslPolicyErrors)
{
    bool isChainValid = certificateChain.Build(cert);
    if (!isChainValid) return false;

    var isCertSignedByTheTlsCert = certificateChain.ChainElements.Cast<X509ChainElement>()
        .Any(x => x.Certificate.Thumbprint == ledgerTlsCert.Thumbprint);
    return isCertSignedByTheTlsCert;
}

// Create an HttpClientHandler to use our certValidationCheck function.
var httpHandler = new HttpClientHandler();
httpHandler.ServerCertificateCustomValidationCallback = CertValidationCheck;

// Create the ledger client using a transport that uses our custom ServerCertificateCustomValidationCallback.
var options = new ConfidentialLedgerClientOptions { Transport = new HttpClientTransport(httpHandler) };
var ledgerClient = new ConfidentialLedgerClient(TestEnvironment.ConfidentialLedgerUrl, new DefaultAzureCredential(), options);

Key concepts

Ledger entries

Every write to Confidential Ledger generates an immutable ledger entry in the service. Writes are uniquely identified by transaction ids that increment with each write.

PostLedgerEntryOperation postOperation = ledgerClient.PostLedgerEntry(
    RequestContent.Create(
        new { contents = "Hello world!" }),
    waitForCompletion: true);

string transactionId = postOperation.Id;
Console.WriteLine($"Appended transaction with Id: {transactionId}");

Since Confidential Ledger is a distributed system, rare transient failures may cause writes to be lost. For entries that must be preserved, it is advisable to verify that the write became durable. Note: It may be necessary to call GetTransactionStatus multiple times until it returns a "Committed" status. However, when calling PostLedgerEntry, a successful result indicates that the status is "Committed".

Response statusResponse = ledgerClient.GetTransactionStatus(transactionId);

string status = JsonDocument.Parse(statusResponse.Content)
    .RootElement
    .GetProperty("state")
    .GetString();

Console.WriteLine($"Transaction status: {status}");

// Wait for the entry to be committed
while (status == "Pending")
{
    statusResponse = ledgerClient.GetTransactionStatus(transactionId);
    status = JsonDocument.Parse(statusResponse.Content)
        .RootElement
        .GetProperty("state")
        .GetString();
}

Console.WriteLine($"Transaction status: {status}");
Receipts

State changes to the Confidential Ledger are saved in a data structure called a Merkle tree. To cryptographically verify that writes were correctly saved, a Merkle proof, or receipt, can be retrieved for any transaction id.

Response receiptResponse = ledgerClient.GetReceipt(transactionId);
string receiptJson = new StreamReader(receiptResponse.ContentStream).ReadToEnd();

Console.WriteLine(receiptJson);
Sub-ledgers

While most use cases will involve one ledger, we provide the sub-ledger feature in case different logical groups of data need to be stored in the same Confidential Ledger.

ledgerClient.PostLedgerEntry(
    RequestContent.Create(
        new { contents = "Hello from Chris!", subLedgerId = "Chris' messages" }),
    waitForCompletion: true);

ledgerClient.PostLedgerEntry(
    RequestContent.Create(
        new { contents = "Hello from Allison!", subLedgerId = "Allison's messages" }),
    waitForCompletion: true);

When no sub-ledger id is specified on method calls, the Confidential Ledger service will assume a constant, service-determined sub-ledger id.

Response postResponse = ledgerClient.PostLedgerEntry(
    RequestContent.Create(
        new { contents = "Hello world!" }),
    waitForCompletion: true);
string transactionId = postOperation.Id;
string subLedgerId = "subledger:0";

// Provide both the transactionId and subLedgerId.
Response getBySubledgerResponse = ledgerClient.GetLedgerEntry(transactionId,  subLedgerId);

// Try until the entry is available.
bool loaded = false;
JsonElement element = default;
string contents = null;
while (!loaded)
{
    loaded = JsonDocument.Parse(getBySubledgerResponse.Content)
        .RootElement
        .TryGetProperty("entry", out element);
    if (loaded)
    {
        contents = element.GetProperty("contents").GetString();
    }
    else
    {
        getBySubledgerResponse = ledgerClient.GetLedgerEntry(transactionId, subLedgerId);
    }
}

Console.WriteLine(contents); // "Hello world!"

// Now just provide the transactionId.
getBySubledgerResponse = ledgerClient.GetLedgerEntry(transactionId);

string subLedgerId2 = JsonDocument.Parse(getBySubledgerResponse.Content)
    .RootElement
    .GetProperty("entry")
    .GetProperty("subLedgerId")
    .GetString();

Console.WriteLine($"{subLedgerId} == {subLedgerId2}");

Ledger entries are retrieved from sub-ledgers. When a transaction id is specified, the returned value is the value contained in the specified sub-ledger at the point in time identified by the transaction id. If no transaction id is specified, the latest available value is returned.

PostLedgerEntryOperation firstPostOperation = ledgerClient.PostLedgerEntry(
    RequestContent.Create(new { contents = "Hello world 0" }),
    waitForCompletion: true);
ledgerClient.PostLedgerEntry(
    RequestContent.Create(new { contents = "Hello world 1" }),
    waitForCompletion: true);
PostLedgerEntryOperation subLedgerPostOperation = ledgerClient.PostLedgerEntry(
    RequestContent.Create(new { contents = "Hello world sub-ledger 0" }),
    "my sub-ledger",
    waitForCompletion: true);
ledgerClient.PostLedgerEntry(
    RequestContent.Create(new { contents = "Hello world sub-ledger 1" }),
    "my sub-ledger",
    waitForCompletion: true);

string transactionId = firstPostOperation.Id;

// Wait for the entry to be committed
status = "Pending";
while (status == "Pending")
{
    statusResponse = ledgerClient.GetTransactionStatus(transactionId);
    status = JsonDocument.Parse(statusResponse.Content)
        .RootElement
        .GetProperty("state")
        .GetString();
}

// The ledger entry written at the transactionId in firstResponse is retrieved from the default sub-ledger.
Response getResponse = ledgerClient.GetLedgerEntry(transactionId);

// Try until the entry is available.
loaded = false;
element = default;
contents = null;
while (!loaded)
{
    loaded = JsonDocument.Parse(getResponse.Content)
        .RootElement
        .TryGetProperty("entry", out element);
    if (loaded)
    {
        contents = element.GetProperty("contents").GetString();
    }
    else
    {
        getResponse = ledgerClient.GetLedgerEntry(transactionId, subLedgerId);
    }
}

string firstEntryContents = JsonDocument.Parse(getResponse.Content)
    .RootElement
    .GetProperty("entry")
    .GetProperty("contents")
    .GetString();

Console.WriteLine(firstEntryContents); // "Hello world 0"

// This will return the latest entry available in the default sub-ledger.
getResponse = ledgerClient.GetCurrentLedgerEntry();

// Try until the entry is available.
loaded = false;
element = default;
string latestDefaultSubLedger = null;
while (!loaded)
{
    loaded = JsonDocument.Parse(getResponse.Content)
        .RootElement
        .TryGetProperty("contents", out element);
    if (loaded)
    {
        latestDefaultSubLedger = element.GetString();
    }
    else
    {
        getResponse = ledgerClient.GetCurrentLedgerEntry();
    }
}

Console.WriteLine($"The latest ledger entry from the default sub-ledger is {latestDefaultSubLedger}"); //"Hello world 1"

// The ledger entry written at subLedgerTransactionId is retrieved from the sub-ledger 'sub-ledger'.
string subLedgerTransactionId = subLedgerPostOperation.Id;

getResponse = ledgerClient.GetLedgerEntry(subLedgerTransactionId, "my sub-ledger");
// Try until the entry is available.
loaded = false;
element = default;
string subLedgerEntry = null;
while (!loaded)
{
    loaded = JsonDocument.Parse(getResponse.Content)
        .RootElement
        .TryGetProperty("entry", out element);
    if (loaded)
    {
        subLedgerEntry = element.GetProperty("contents").GetString();
    }
    else
    {
        getResponse = ledgerClient.GetLedgerEntry(subLedgerTransactionId, "my sub-ledger");
    }
}

Console.WriteLine(subLedgerEntry); // "Hello world sub-ledger 0"

// This will return the latest entry available in the sub-ledger.
getResponse = ledgerClient.GetCurrentLedgerEntry("my sub-ledger");
string latestSubLedger = JsonDocument.Parse(getResponse.Content)
    .RootElement
    .GetProperty("contents")
    .GetString();

Console.WriteLine($"The latest ledger entry from the sub-ledger is {latestSubLedger}"); // "Hello world sub-ledger 1"
Ranged queries

Ledger entries in a sub-ledger may be retrieved over a range of transaction ids. Note: Both ranges are optional; they can be provided individually or not at all.

ledgerClient.GetLedgerEntries(fromTransactionId: "2.1", toTransactionId: subLedgerTransactionId);

User management

Users are managed directly with the Confidential Ledger instead of through Azure. New users may be AAD-based or certificate-based.

string newUserAadObjectId = "<some AAD user or service princpal object Id>";
ledgerClient.CreateOrUpdateUser(
    newUserAadObjectId,
    RequestContent.Create(new { assignedRole = "Reader" }));

Confidential consortium and enclave verifications

One may want to validate details about the Confidential Ledger for a variety of reasons. For example, you may want to view details about how Microsoft may manage your Confidential Ledger as part of Confidential Consortium Framework governance, or verify that your Confidential Ledger is indeed running in SGX enclaves. A number of client methods are provided for these use cases.

Response consortiumResponse = ledgerClient.GetConsortiumMembers();
string membersJson = new StreamReader(consortiumResponse.ContentStream).ReadToEnd();

// Consortium members can manage and alter the Confidential Ledger, such as by replacing unhealthy nodes.
Console.WriteLine(membersJson);

// The constitution is a collection of JavaScript code that defines actions available to members,
// and vets proposals by members to execute those actions.
Response constitutionResponse = ledgerClient.GetConstitution();
string constitutionJson = new StreamReader(constitutionResponse.ContentStream).ReadToEnd();

Console.WriteLine(constitutionJson);

// Enclave quotes contain material that can be used to cryptographically verify the validity and contents of an enclave.
Response enclavesResponse = ledgerClient.GetEnclaveQuotes();
string enclavesJson = new StreamReader(enclavesResponse.ContentStream).ReadToEnd();

Console.WriteLine(enclavesJson);

Microsoft Azure Attestation Service is one provider of SGX enclave quotes.

Thread safety

We guarantee that all client instance methods are thread-safe and independent of each other (guideline). This ensures that the recommendation of reusing client instances is always safe, even across threads.

Additional concepts

Client options | Accessing the response | Long-running operations | Handling failures | Diagnostics | Mocking | Client lifetime

Examples

Coming Soon...

Troubleshooting

Response values returned from Confidential Ledger client methods are Response objects, which contain information about the http response such as the http Status property and a Headers object containing more information about the failure.

Setting up console logging

The simplest way to see the logs is to enable the console logging. To create an Azure SDK log listener that outputs messages to console use AzureEventSourceListener.CreateConsoleLogger method.

// Setup a listener to monitor logged events.
using AzureEventSourceListener listener = AzureEventSourceListener.CreateConsoleLogger();

To learn more about other logging mechanisms see here.

Next steps

For more extensive documentation on Azure Confidential Ledger, see the API reference documentation. You may also read more about Microsoft Research's open-source Confidential Consortium Framework.

Contributing

This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit cla.microsoft.com.

This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.3.0 7,751 2/5/2024
1.2.0 12,703 9/12/2023
1.1.0 16,646 11/9/2022
1.1.0-beta.1 777 8/10/2022
1.0.0 4,861 7/18/2022
1.0.0-beta.3 248 7/8/2022
1.0.0-beta.2 1,882 6/8/2021