Skip to content

Commit f894070

Browse files
abergsmackie1001aseigler
authored
Release version 2.0.0 (#175)
* Version 2.0.0 * Fixed SANFromAttnCertExts to correctly extract the TPM properties from the Subject Alternative Name certificate extension (#187) * Fixed SANFromAttnCertExts to correctly extract the TPM properties from the Subject Alternative Name certificate extension * Variable name typo * Add check and fix for non-conformant SAN attribute in AIK cert, write test for same. Co-authored-by: Alex Seigler <alexseigler@hotmail.com> * MDS error handling and cache-friendlieness refactoring (#188) * Improved error handling and logging for MDS errors along with a refactoring of how the TOC JWT alg is passed around to better serve the cached use-case * Updated test * Update ConformanceMetadataRepository.cs * Update Tpm.cs Co-authored-by: Paul McNamara <paul.mcnamara@theaccessgroup.com> Co-authored-by: Alex Seigler <alexseigler@hotmail.com>
1 parent 3d823eb commit f894070

File tree

13 files changed

+608
-363
lines changed

13 files changed

+608
-363
lines changed

Demo/Startup.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,12 @@ public void ConfigureServices(IServiceCollection services)
5353
.AddCachedMetadataService(config =>
5454
{
5555
//They'll be used in a "first match wins" way in the order registered
56-
config.AddStaticMetadataRepository();
56+
5757
if (!string.IsNullOrWhiteSpace(Configuration["fido2:MDSAccessKey"]))
5858
{
5959
config.AddFidoMetadataRepository(Configuration["fido2:MDSAccessKey"]);
6060
}
61+
config.AddStaticMetadataRepository();
6162
});
6263

6364

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project>
22
<!-- Package Metadata -->
33
<PropertyGroup>
4-
<VersionPrefix>1.1.0</VersionPrefix>
4+
<VersionPrefix>2.0.0</VersionPrefix>
55
<VersionSuffix>
66
</VersionSuffix>
77
<Description>FIDO2 .NET library (WebAuthn)</Description>

Src/Fido2.AspNet/DistributedCacheMetadataService.cs

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -71,33 +71,48 @@ protected virtual string GetEntryCacheKey(IMetadataRepository repository, Guid a
7171
return $"{CACHE_PREFIX}:{repository.GetType().Name}:Entry:{aaGuid}";
7272
}
7373

74-
protected virtual async Task LoadEntryStatement(IMetadataRepository repository, MetadataTOCPayloadEntry entry, DateTime? cacheUntil = null)
74+
protected virtual async Task LoadTocEntryStatement(
75+
IMetadataRepository repository,
76+
MetadataTOCPayload toc,
77+
MetadataTOCPayloadEntry entry,
78+
DateTime? cacheUntil = null)
7579
{
76-
if (entry.AaGuid != null)
80+
if (entry.AaGuid != null && !_entries.ContainsKey(Guid.Parse(entry.AaGuid)))
7781
{
78-
var cacheKey = GetEntryCacheKey(repository, Guid.Parse(entry.AaGuid));
82+
var entryAaGuid = Guid.Parse(entry.AaGuid);
83+
84+
var cacheKey = GetEntryCacheKey(repository, entryAaGuid);
7985

8086
var cachedEntry = await _cache.GetStringAsync(cacheKey);
8187
if (cachedEntry != null)
8288
{
8389
var statement = JsonConvert.DeserializeObject<MetadataStatement>(cachedEntry);
8490
if (!string.IsNullOrWhiteSpace(statement.AaGuid))
85-
_metadataStatements.TryAdd(Guid.Parse(statement.AaGuid), statement);
91+
{
92+
var aaGuid = Guid.Parse(statement.AaGuid);
93+
_metadataStatements.TryAdd(aaGuid, statement);
94+
_entries.TryAdd(aaGuid, entry);
95+
}
8696
}
8797
else
8898
{
89-
_log?.LogInformation("Entry for {0}/{1} not cached so loading from MDS...", entry.AaGuid, entry.Aaid);
99+
_log?.LogInformation("Entry for {0} {1} not cached so loading from MDS...", entry.AaGuid, entry.MetadataStatement?.Description ?? entry.StatusReports?.FirstOrDefault().CertificationDescriptor ?? "(unknown)");
90100

91101
try
92102
{
93-
var statement = await repository.GetMetadataStatement(entry);
103+
var statement = await repository.GetMetadataStatement(toc, entry);
94104

95105
if (!string.IsNullOrWhiteSpace(statement.AaGuid))
96106
{
97-
_metadataStatements.TryAdd(Guid.Parse(statement.AaGuid), statement);
98-
99107
var statementJson = JsonConvert.SerializeObject(statement, Formatting.Indented);
100108

109+
_log?.LogDebug("{0}:{1}\n{2}", statement.AaGuid, statement.Description, statementJson);
110+
111+
var aaGuid = Guid.Parse(statement.AaGuid);
112+
113+
_metadataStatements.TryAdd(aaGuid, statement);
114+
_entries.TryAdd(aaGuid, entry);
115+
101116
if (cacheUntil.HasValue)
102117
{
103118
await _cache.SetStringAsync(cacheKey, statementJson, new DistributedCacheEntryOptions
@@ -107,7 +122,7 @@ protected virtual async Task LoadEntryStatement(IMetadataRepository repository,
107122
}
108123
}
109124
}
110-
catch(Exception ex)
125+
catch (Exception ex)
111126
{
112127
_log?.LogError(ex, "Error getting MetadataStatement from {0} for AAGUID '{1}' ", repository.GetType().Name, entry.AaGuid);
113128
throw;
@@ -136,7 +151,7 @@ protected virtual async Task LoadEntryStatement(IMetadataRepository repository,
136151
return null;
137152
}
138153

139-
protected virtual async Task InitializeClient(IMetadataRepository repository)
154+
protected virtual async Task InitializeRepository(IMetadataRepository repository)
140155
{
141156
var tocCacheKey = GetTocCacheKey(repository);
142157

@@ -153,23 +168,23 @@ protected virtual async Task InitializeClient(IMetadataRepository repository)
153168
}
154169
else
155170
{
156-
_log?.LogInformation("TOC not cached so loading from MDS...");
171+
_log?.LogInformation($"TOC for {repository.GetType().Name} not cached so loading from MDS...");
157172

158173
try
159174
{
160175
toc = await repository.GetToc();
161176
}
162-
catch(Exception ex)
177+
catch (Exception ex)
163178
{
164179
_log?.LogError(ex, "Error getting TOC from {0}", repository.GetType().Name);
165180
throw;
166181
}
167182

168-
_log?.LogInformation("TOC not cached so loading from MDS... Done.");
183+
_log?.LogInformation($"TOC for {repository.GetType().Name} not cached so loading from MDS... Done.");
169184

170185
cacheUntil = GetCacheUntilTime(toc);
171186

172-
if(cacheUntil.HasValue)
187+
if (cacheUntil.HasValue)
173188
{
174189
await _cache.SetStringAsync(
175190
tocCacheKey,
@@ -183,29 +198,32 @@ await _cache.SetStringAsync(
183198

184199
foreach (var entry in toc.Entries)
185200
{
186-
if (!string.IsNullOrEmpty(entry.AaGuid))
201+
if (!string.IsNullOrEmpty(entry.AaGuid)) //Only load FIDO2 entries
187202
{
188-
if(_entries.TryAdd(Guid.Parse(entry.AaGuid), entry))
203+
try
204+
{
205+
await LoadTocEntryStatement(repository, toc, entry, cacheUntil);
206+
}
207+
catch (Exception ex)
189208
{
190-
//Load if it doesn't already exist
191-
await LoadEntryStatement(repository, entry, cacheUntil);
209+
_log?.LogError(ex, "Error getting statement from {0} for AAGUID '{1}'.\nTOC entry:\n{2} ", repository.GetType().Name, entry.AaGuid, JsonConvert.SerializeObject(entry, Formatting.Indented));
192210
}
193211
}
194212
}
195213
}
196214

197215
public virtual async Task Initialize()
198216
{
199-
foreach (var client in _repositories)
217+
foreach (var repository in _repositories)
200218
{
201219
try
202220
{
203-
await InitializeClient(client);
221+
await InitializeRepository(repository);
204222
}
205223
catch (Exception ex)
206224
{
207225
//Catch and log this as we don't want issues with external services to prevent app startup
208-
_log.LogCritical(ex, "Error initialising MDS client '{0}'", client.GetType().Name);
226+
_log?.LogCritical(ex, "Error initialising MDS client '{0}'", repository.GetType().Name);
209227
}
210228
}
211229
_initialized = true;

Src/Fido2.Models/Metadata/MetadataTOCPayload.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class MetadataTOCPayload
1818
/// </remarks>
1919
[JsonProperty("legalHeader")]
2020
public string LegalHeader { get; set; }
21-
/// <summary>
21+
/// <summary>
2222
/// Gets or sets the serial number of this UAF Metadata TOC Payload.
2323
/// </summary>
2424
/// <remarks>
@@ -35,6 +35,12 @@ public class MetadataTOCPayload
3535
/// Gets or sets a list of zero or more entries of <see cref="MetadataTOCPayloadEntry"/>.
3636
/// </summary>
3737
[JsonProperty("entries", Required = Required.Always)]
38-
public MetadataTOCPayloadEntry[] Entries { get; set; }
38+
public MetadataTOCPayloadEntry[] Entries { get; set; }
39+
40+
/// <summary>
41+
/// The "alg" property from the original JWT header. Used to validate MetadataStatements.
42+
/// </summary>
43+
[JsonProperty("jwtAlg", Required = Required.AllowNull)]
44+
public string JwtAlg { get; set; }
3945
}
4046
}

Src/Fido2/AttestationFormat/Tpm.cs

Lines changed: 97 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ internal class Tpm : AttestationVerifier
4040
"id:57454300", // 'WEC' Winbond
4141
"id:524F4343", // 'ROCC' Fuzhou Rockchip
4242
"id:474F4F47", // 'GOOG' Google
43-
};
43+
};
44+
4445
public override (AttestationType, X509Certificate2[]) Verify()
4546
{
4647
// 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields.
@@ -57,7 +58,7 @@ public override (AttestationType, X509Certificate2[]) Verify()
5758
if (null != attStmt["pubArea"] &&
5859
CBORType.ByteString == attStmt["pubArea"].Type &&
5960
0 != attStmt["pubArea"].GetByteString().Length)
60-
{
61+
{
6162
pubArea = new PubArea(attStmt["pubArea"].GetByteString());
6263
}
6364

@@ -96,7 +97,7 @@ public override (AttestationType, X509Certificate2[]) Verify()
9697
if (null != attStmt["certInfo"] &&
9798
CBORType.ByteString == attStmt["certInfo"].Type &&
9899
0 != attStmt["certInfo"].GetByteString().Length)
99-
{
100+
{
100101
certInfo = new CertInfo(attStmt["certInfo"].GetByteString());
101102
}
102103

@@ -115,7 +116,7 @@ public override (AttestationType, X509Certificate2[]) Verify()
115116

116117
using(var hasher = CryptoUtils.GetHasher(CryptoUtils.HashAlgFromCOSEAlg(Alg.AsInt32())))
117118
{
118-
if (!hasher.ComputeHash(Data).SequenceEqual(certInfo.ExtraData))
119+
if (!hasher.ComputeHash(Data).SequenceEqual(certInfo.ExtraData))
119120
throw new Fido2VerificationException("Hash value mismatch extraData and attToBeSigned");
120121
}
121122

@@ -222,73 +223,126 @@ public override (AttestationType, X509Certificate2[]) Verify()
222223
{ 2, TpmEccCurve.TPM_ECC_NIST_P384},
223224
{ 3, TpmEccCurve.TPM_ECC_NIST_P521}
224225
};
225-
private static (string, string, string) SANFromAttnCertExts(X509ExtensionCollection exts)
226+
private static (string, string, string) SANFromAttnCertExts(X509ExtensionCollection extensions)
226227
{
227228
string tpmManufacturer = string.Empty,
228229
tpmModel = string.Empty,
229230
tpmVersion = string.Empty;
230-
231+
231232
var foundSAN = false;
232233

233-
foreach (var ext in exts)
234+
foreach (var extension in extensions)
234235
{
235-
if (ext.Oid.Value.Equals("2.5.29.17")) // subject alternative name
236+
if (extension.Oid.Value.Equals("2.5.29.17")) // subject alternative name
236237
{
237-
if (0 == ext.RawData.Length)
238+
if (0 == extension.RawData.Length)
238239
throw new Fido2VerificationException("SAN missing from TPM attestation certificate");
239240

240241
foundSAN = true;
241-
var san = AsnElt.Decode(ext.RawData);
242-
san.CheckTag(AsnElt.SEQUENCE);
243-
san.CheckConstructed();
244-
foreach (AsnElt generalName in san.Sub)
245-
{
246-
if (generalName.TagClass != AsnElt.CONTEXT || generalName.TagValue != AsnElt.OCTET_STRING)
247-
continue;
248242

243+
var subjectAlternativeName = AsnElt.Decode(extension.RawData);
244+
subjectAlternativeName.CheckConstructed();
245+
subjectAlternativeName.CheckTag(AsnElt.SEQUENCE);
246+
subjectAlternativeName.CheckNumSubMin(1);
247+
248+
var generalName = subjectAlternativeName.Sub.FirstOrDefault(o => o.TagClass == AsnElt.CONTEXT && o.TagValue == AsnElt.OCTET_STRING);
249+
250+
if (generalName != null)
251+
{
249252
generalName.CheckConstructed();
250253
generalName.CheckNumSub(1);
251-
252-
var exp = generalName.GetSub(0);
253-
exp.CheckConstructed();
254-
exp.CheckNumSub(1);
255-
exp.CheckTag(AsnElt.SEQUENCE);
256-
257-
var directoryName = exp.GetSub(0);
258-
directoryName.CheckConstructed();
259-
directoryName.CheckNumSub(3);
260-
directoryName.CheckTag(AsnElt.SET);
261-
262-
foreach (AsnElt dn in directoryName.Sub)
254+
255+
var nameSequence = generalName.GetSub(0);
256+
nameSequence.CheckConstructed();
257+
nameSequence.CheckTag(AsnElt.SEQUENCE);
258+
nameSequence.CheckNumSubMin(1);
259+
260+
/*
261+
262+
Per Trusted Computing Group Endorsement Key Credential Profile section 3.2.9:
263+
264+
"The issuer MUST include TPM manufacturer, TPM part number and TPM firmware version, using the directoryName-form within the GeneralName structure. The ASN.1 encoding is specified in section 3.1.2 TPM Device Attributes."
265+
266+
An example is provided in document section A.1 Example 1:
267+
268+
// SEQUENCE
269+
30 49
270+
// SET
271+
31 16
272+
// SEQUENCE
273+
30 14
274+
// OBJECT IDENTIFER tcg-at-tpmManufacturer (2.23.133.2.1)
275+
06 05 67 81 05 02 01
276+
// UTF8 STRING id:54434700 (TCG)
277+
0C 0B 69 64 3A 35 34 34 33 34 37 30 30
278+
// SET
279+
31 17
280+
// SEQUENCE
281+
30 15
282+
// OBJECT IDENTIFER tcg-at-tpmModel (2.23.133.2.2)
283+
06 05 67 81 05 02 02
284+
// UTF8 STRING ABCDEF123456
285+
0C 0C 41 42 43 44 45 46 31 32 33 34 35 36
286+
// SET
287+
31 16
288+
// SEQUENCE
289+
30 14
290+
// OBJECT IDENTIFER tcg-at-tpmVersion (2.23.133.2.3)
291+
06 05 67 81 05 02 03
292+
// UTF8 STRING id:00010023
293+
0C 0B 69 64 3A 30 30 30 31 30 30 32 33
294+
295+
Some TPM implementations place each device attributes SEQUENCE within a single SET instead of each in its own SET.
296+
297+
This detects this condition and repacks each devices attributes SEQUENCE into its own SET to conform with TCG spec.
298+
299+
*/
300+
301+
var deviceAttributes = nameSequence.Sub;
302+
if (1 != deviceAttributes.FirstOrDefault().Sub.Length)
263303
{
264-
dn.CheckNumSub(2);
265-
dn.CheckTag(AsnElt.SEQUENCE);
266-
var oid = dn.GetSub(0);
267-
oid.CheckTag(AsnElt.OBJECT_IDENTIFIER);
268-
oid.CheckPrimitive();
269-
270-
var value = dn.GetSub(1);
271-
value.CheckTag(AsnElt.UTF8String);
272-
oid.CheckPrimitive();
273-
switch (oid.GetOID())
304+
deviceAttributes = deviceAttributes.FirstOrDefault().Sub.Select(o => AsnElt.Make(AsnElt.SET, o)).ToArray();
305+
}
306+
307+
foreach (AsnElt propertySet in deviceAttributes)
308+
{
309+
propertySet.CheckTag(AsnElt.SET);
310+
propertySet.CheckNumSub(1);
311+
312+
var propertySequence = propertySet.GetSub(0);
313+
propertySequence.CheckTag(AsnElt.SEQUENCE);
314+
propertySequence.CheckNumSub(2);
315+
316+
var propertyOid = propertySequence.GetSub(0);
317+
propertyOid.CheckTag(AsnElt.OBJECT_IDENTIFIER);
318+
propertyOid.CheckPrimitive();
319+
320+
var propertyValue = propertySequence.GetSub(1);
321+
propertyValue.CheckTag(AsnElt.UTF8String);
322+
propertyValue.CheckPrimitive();
323+
324+
switch (propertyOid.GetOID())
274325
{
275326
case ("2.23.133.2.1"):
276-
tpmManufacturer = value.GetString();
327+
tpmManufacturer = propertyValue.GetString();
277328
break;
278329
case ("2.23.133.2.2"):
279-
tpmModel = value.GetString();
330+
tpmModel = propertyValue.GetString();
280331
break;
281332
case ("2.23.133.2.3"):
282-
tpmVersion = value.GetString();
333+
tpmVersion = propertyValue.GetString();
283334
break;
284335
default:
285336
continue;
286337
}
287338
}
288339
}
340+
341+
break;
289342
}
290343
}
291-
if (false == foundSAN)
344+
345+
if (!foundSAN)
292346
throw new Fido2VerificationException("SAN missing from TPM attestation certificate");
293347

294348
return (tpmManufacturer, tpmModel, tpmVersion);
@@ -304,7 +358,7 @@ private static bool EKUFromAttnCertExts(X509ExtensionCollection exts, string exp
304358
if (expectedEnhancedKeyUsages.Equals(oid.Value))
305359
return true;
306360
}
307-
361+
308362
}
309363
}
310364
return false;

0 commit comments

Comments
 (0)