Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### Features Added

- A new option to pass transparent statement verification key sets mapped to domain names for offline verification using `CodeTransparencyVerificationOptions.OfflineKeys`
- A new option to restrict the use of a network resolution of the ledger keys when using `OfflineKeys` with `CodeTransparencyVerificationOptions.OfflineKeysBehavior`

### Breaking Changes

### Bugs Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ public enum ServiceVersion
V2025_01_31_Preview = 1,
}
}
public sealed partial class CodeTransparencyOfflineKeys
{
public CodeTransparencyOfflineKeys() { }
public System.Collections.Generic.IReadOnlyDictionary<string, Azure.Security.CodeTransparency.JwksDocument> ByDomain { get { throw null; } }
public void Add(string ledgerDomain, Azure.Security.CodeTransparency.JwksDocument jwksDocument) { }
public static Azure.Security.CodeTransparency.CodeTransparencyOfflineKeys FromBinaryData(System.BinaryData json) { throw null; }
}
public enum CodeTransparencyOperationStatus
{
Running = 0,
Expand All @@ -85,6 +92,8 @@ public sealed partial class CodeTransparencyVerificationOptions
public CodeTransparencyVerificationOptions() { }
public System.Collections.Generic.IList<string> AuthorizedDomains { get { throw null; } set { } }
public Azure.Security.CodeTransparency.AuthorizedReceiptBehavior AuthorizedReceiptBehavior { get { throw null; } set { } }
public Azure.Security.CodeTransparency.CodeTransparencyOfflineKeys OfflineKeys { get { throw null; } set { } }
public Azure.Security.CodeTransparency.OfflineKeysBehavior OfflineKeysBehavior { get { throw null; } set { } }
public Azure.Security.CodeTransparency.UnauthorizedReceiptBehavior UnauthorizedReceiptBehavior { get { throw null; } set { } }
}
public partial class JsonWebKey : System.ClientModel.Primitives.IJsonModel<Azure.Security.CodeTransparency.JsonWebKey>, System.ClientModel.Primitives.IPersistableModel<Azure.Security.CodeTransparency.JsonWebKey>
Expand Down Expand Up @@ -125,6 +134,11 @@ protected virtual void JsonModelWriteCore(System.Text.Json.Utf8JsonWriter writer
string System.ClientModel.Primitives.IPersistableModel<Azure.Security.CodeTransparency.JwksDocument>.GetFormatFromOptions(System.ClientModel.Primitives.ModelReaderWriterOptions options) { throw null; }
System.BinaryData System.ClientModel.Primitives.IPersistableModel<Azure.Security.CodeTransparency.JwksDocument>.Write(System.ClientModel.Primitives.ModelReaderWriterOptions options) { throw null; }
}
public enum OfflineKeysBehavior
{
FallbackToNetwork = 0,
NoFallbackToNetwork = 1,
}
public static partial class SecurityCodeTransparencyModelFactory
{
public static Azure.Security.CodeTransparency.JsonWebKey JsonWebKey(string alg = null, string crv = null, string d = null, string dp = null, string dq = null, string e = null, string k = null, string kid = null, string kty = null, string n = null, string p = null, string q = null, string qi = null, string use = null, string x = null, System.Collections.Generic.IEnumerable<string> x5c = null, string y = null) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ public enum ServiceVersion
V2025_01_31_Preview = 1,
}
}
public sealed partial class CodeTransparencyOfflineKeys
{
public CodeTransparencyOfflineKeys() { }
public System.Collections.Generic.IReadOnlyDictionary<string, Azure.Security.CodeTransparency.JwksDocument> ByDomain { get { throw null; } }
public void Add(string ledgerDomain, Azure.Security.CodeTransparency.JwksDocument jwksDocument) { }
public static Azure.Security.CodeTransparency.CodeTransparencyOfflineKeys FromBinaryData(System.BinaryData json) { throw null; }
}
public enum CodeTransparencyOperationStatus
{
Running = 0,
Expand All @@ -85,6 +92,8 @@ public sealed partial class CodeTransparencyVerificationOptions
public CodeTransparencyVerificationOptions() { }
public System.Collections.Generic.IList<string> AuthorizedDomains { get { throw null; } set { } }
public Azure.Security.CodeTransparency.AuthorizedReceiptBehavior AuthorizedReceiptBehavior { get { throw null; } set { } }
public Azure.Security.CodeTransparency.CodeTransparencyOfflineKeys OfflineKeys { get { throw null; } set { } }
public Azure.Security.CodeTransparency.OfflineKeysBehavior OfflineKeysBehavior { get { throw null; } set { } }
public Azure.Security.CodeTransparency.UnauthorizedReceiptBehavior UnauthorizedReceiptBehavior { get { throw null; } set { } }
}
public partial class JsonWebKey : System.ClientModel.Primitives.IJsonModel<Azure.Security.CodeTransparency.JsonWebKey>, System.ClientModel.Primitives.IPersistableModel<Azure.Security.CodeTransparency.JsonWebKey>
Expand Down Expand Up @@ -125,6 +134,11 @@ protected virtual void JsonModelWriteCore(System.Text.Json.Utf8JsonWriter writer
string System.ClientModel.Primitives.IPersistableModel<Azure.Security.CodeTransparency.JwksDocument>.GetFormatFromOptions(System.ClientModel.Primitives.ModelReaderWriterOptions options) { throw null; }
System.BinaryData System.ClientModel.Primitives.IPersistableModel<Azure.Security.CodeTransparency.JwksDocument>.Write(System.ClientModel.Primitives.ModelReaderWriterOptions options) { throw null; }
}
public enum OfflineKeysBehavior
{
FallbackToNetwork = 0,
NoFallbackToNetwork = 1,
}
public static partial class SecurityCodeTransparencyModelFactory
{
public static Azure.Security.CodeTransparency.JsonWebKey JsonWebKey(string alg = null, string crv = null, string d = null, string dp = null, string dq = null, string e = null, string k = null, string kid = null, string kty = null, string n = null, string p = null, string q = null, string qi = null, string use = null, string x = null, System.Collections.Generic.IEnumerable<string> x5c = null, string y = null) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@

using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.Formats.Cbor;
using System.IO;
using System.Security.Cryptography.Cose;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
Expand All @@ -32,6 +37,16 @@ public partial class CodeTransparencyClient
/// </summary>
public static readonly string UnknownIssuerPrefix = "__unknown-issuer::";

/// <summary>
/// Public key storage used to verify receipts. The value can be set through the verification options.
/// </summary>
private IReadOnlyDictionary<string, JwksDocument> _offlineKeys = null;

/// <summary>
/// Indicates whether offline keys can fallback to network retrieval when a key is not found locally.
/// </summary>
private bool _offlineKeysAllowNetworkFallback = true;

/// <summary>
/// Initializes a new instance of CodeTransparencyClient. The client will download its own
/// TLS CA cert to perform server cert authentication.
Expand Down Expand Up @@ -420,10 +435,16 @@ public static void VerifyTransparentStatement(byte[] transparentStatementCoseSig
if (!clientInstances.TryGetValue(issuer, out CodeTransparencyClient clientInstance))
{
clientInstance = new CodeTransparencyClient(new Uri($"https://{issuer}"), clientOptions);
if (verificationOptions?.OfflineKeys != null)
{
clientInstance._offlineKeys = verificationOptions.OfflineKeys.ByDomain;
clientInstance._offlineKeysAllowNetworkFallback = verificationOptions.OfflineKeysBehavior == OfflineKeysBehavior.FallbackToNetwork;
}
clientInstances[issuer] = clientInstance;
}
clientInstance.RunTransparentStatementVerification(transparentStatementCoseSign1Bytes, receiptBytes);

// If we reach here, verification succeeded
if (isAuthorized)
{
validAuthorizedDomainsEncountered.Add(issuer);
Expand Down Expand Up @@ -508,8 +529,19 @@ private JsonWebKey GetServiceCertificateKey(byte[] receiptBytes)
throw new InvalidOperationException("Issuer and service instance name are not matching.");
}

// Get all the public keys from the JWKS endpoint
JwksDocument jwksDocument = GetPublicKeys().Value;
JwksDocument jwksDocument = null;
// Check if we have offline keys for this domain
if (_offlineKeys?.TryGetValue(issuer, out jwksDocument) != true && _offlineKeysAllowNetworkFallback)
{
// Get all the public keys from the JWKS endpoint
jwksDocument = GetPublicKeys().Value;
}

// Ensure jwksDocument was obtained from either offline keys or network
if (jwksDocument == null)
{
throw new InvalidOperationException($"No keys available for issuer '{issuer}'. Either offline keys are not configured or network fallback is disabled.");
}

// Ensure there is at least one entry in the JWKS document
if (jwksDocument.Keys.Count == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public partial class CodeTransparencyClientOptions : ClientOptions
/// The default identity service endpoint.
/// </summary>
public string IdentityClientEndpoint { get; set; } = "https://identity.confidential-ledger.core.azure.com/";

/// <summary>
/// Used in the regular client constructor.
/// Creates the <see cref="CodeTransparencyCertificateClient"/> used to get the identity service certificate.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.Core;

namespace Azure.Security.CodeTransparency
{
/// <summary>
/// A case-insensitive dictionary mapping ledger domains to their JWKS documents for offline verification.
/// </summary>
public sealed class CodeTransparencyOfflineKeys
{
private IDictionary<string, JwksDocument> _keysByDomain;

/// <summary>
/// Initializes a new instance of CodeTransparencyOfflineKeys.
/// </summary>
public CodeTransparencyOfflineKeys()
{
_keysByDomain = new Dictionary<string, JwksDocument>(StringComparer.OrdinalIgnoreCase);
}

/// <summary>
/// Gets the dictionary of ledger domains to their JWKS documents.
/// </summary>
public IReadOnlyDictionary<string, JwksDocument> ByDomain => new ReadOnlyDictionary<string, JwksDocument>(_keysByDomain);

/// <summary>
/// Adds or updates a JWKS document for the specified ledger domain.
/// </summary>
public void Add(string ledgerDomain, JwksDocument jwksDocument)
{
_keysByDomain[ledgerDomain] = jwksDocument;
}

/// <summary>
/// Creates a CodeTransparencyOfflineKeys instance from a BinaryData containing JSON.
/// </summary>
public static CodeTransparencyOfflineKeys FromBinaryData(BinaryData json)
{
return FromJsonDocument(JsonDocument.Parse(json.ToString()));
}

internal static CodeTransparencyOfflineKeys FromJsonDocument(JsonDocument jsonDocument)
{
return DeserializeKeys(jsonDocument.RootElement);
}

internal static CodeTransparencyOfflineKeys DeserializeKeys(JsonElement element, ModelReaderWriterOptions options = null)
{
var keys = new CodeTransparencyOfflineKeys();

foreach (var property in element.EnumerateObject())
{
var ledgerDomain = property.Name;
var jwksDocument = JwksDocument.DeserializeJwksDocument(property.Value, options);
keys.Add(ledgerDomain, jwksDocument);
}

return keys;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public enum AuthorizedReceiptBehavior
/// </summary>
RequireAll = 2
}

/// <summary>
/// Specifies behaviors for receipts whose issuer domains are not contained in <see cref="CodeTransparencyVerificationOptions.AuthorizedDomains"/>.
/// </summary>
Expand All @@ -43,6 +44,21 @@ public enum UnauthorizedReceiptBehavior
FailIfPresent = 2
}

/// <summary>
/// Specifies behaviors for the use of offline keys contained in <see cref="CodeTransparencyVerificationOptions.OfflineKeys"/>.
/// </summary>
public enum OfflineKeysBehavior
{
/// <summary>
/// Use offline keys when available, but fall back to network retrieval if no offline key is found for a given ledger domain.
/// </summary>
FallbackToNetwork = 0,
/// <summary>
/// Use only offline keys. If no offline key is found for a given ledger domain, verification fails.
/// </summary>
NoFallbackToNetwork = 1
}

/// <summary>
/// Options controlling <see cref="CodeTransparencyClient.VerifyTransparentStatement(byte[], CodeTransparencyVerificationOptions, CodeTransparencyClientOptions)"/>.
/// </summary>
Expand Down Expand Up @@ -72,5 +88,17 @@ public CodeTransparencyVerificationOptions()
/// Defaults to <see cref="AuthorizedReceiptBehavior.VerifyAllMatching"/>.
/// </summary>
public AuthorizedReceiptBehavior AuthorizedReceiptBehavior { get; set; } = AuthorizedReceiptBehavior.VerifyAllMatching;

/// <summary>
/// Gets or sets a store mapping ledger domains to JWKS documents for offline verification.
/// When provided, will skip network calls and use the matching JWKS document from this store instead.
/// </summary>
public CodeTransparencyOfflineKeys OfflineKeys { get; set; } = null;

/// <summary>
/// Gets or sets the behavior for using offline keys in <see cref="CodeTransparencyOfflineKeys"/>.
/// Defaults to <see cref="OfflineKeysBehavior.FallbackToNetwork"/>.
/// </summary>
public OfflineKeysBehavior OfflineKeysBehavior { get; set; } = OfflineKeysBehavior.FallbackToNetwork;
}
}
Loading
Loading