From 18886ba3a83577efd11c88cef3fb9c00a3e30f2d Mon Sep 17 00:00:00 2001 From: Ethan Celletti Date: Fri, 22 Sep 2023 01:46:17 +0200 Subject: [PATCH] Adding parsing of query signatures (#93) --- src/Agent/API.md | 56 +++++++++++++++++ src/Agent/API.xml | 30 ++++++++++ src/Agent/Responses/CborUtil.cs | 31 ++++++++++ src/Agent/Responses/NodeSignature.cs | 89 ++++++++++++++++++++++++++++ src/Agent/Responses/QueryResponse.cs | 49 +++++++++------ 5 files changed, 237 insertions(+), 18 deletions(-) create mode 100644 src/Agent/Responses/CborUtil.cs create mode 100644 src/Agent/Responses/NodeSignature.cs diff --git a/src/Agent/API.md b/src/Agent/API.md index 76f68438..8c006ea4 100644 --- a/src/Agent/API.md +++ b/src/Agent/API.md @@ -187,6 +187,11 @@ - [Int](#F-EdjCase-ICP-Agent-Standards-ICRC1-Models-MetaDataValueTag-Int 'EdjCase.ICP.Agent.Standards.ICRC1.Models.MetaDataValueTag.Int') - [Nat](#F-EdjCase-ICP-Agent-Standards-ICRC1-Models-MetaDataValueTag-Nat 'EdjCase.ICP.Agent.Standards.ICRC1.Models.MetaDataValueTag.Nat') - [Text](#F-EdjCase-ICP-Agent-Standards-ICRC1-Models-MetaDataValueTag-Text 'EdjCase.ICP.Agent.Standards.ICRC1.Models.MetaDataValueTag.Text') +- [NodeSignature](#T-EdjCase-ICP-Agent-Responses-NodeSignature 'EdjCase.ICP.Agent.Responses.NodeSignature') + - [#ctor(timestamp,signature,identity)](#M-EdjCase-ICP-Agent-Responses-NodeSignature-#ctor-EdjCase-ICP-Candid-Models-ICTimestamp,System-Byte[],EdjCase-ICP-Candid-Models-Principal- 'EdjCase.ICP.Agent.Responses.NodeSignature.#ctor(EdjCase.ICP.Candid.Models.ICTimestamp,System.Byte[],EdjCase.ICP.Candid.Models.Principal)') + - [Identity](#P-EdjCase-ICP-Agent-Responses-NodeSignature-Identity 'EdjCase.ICP.Agent.Responses.NodeSignature.Identity') + - [Signature](#P-EdjCase-ICP-Agent-Responses-NodeSignature-Signature 'EdjCase.ICP.Agent.Responses.NodeSignature.Signature') + - [Timestamp](#P-EdjCase-ICP-Agent-Responses-NodeSignature-Timestamp 'EdjCase.ICP.Agent.Responses.NodeSignature.Timestamp') - [QueryRejectInfo](#T-EdjCase-ICP-Agent-Responses-QueryRejectInfo 'EdjCase.ICP.Agent.Responses.QueryRejectInfo') - [Code](#P-EdjCase-ICP-Agent-Responses-QueryRejectInfo-Code 'EdjCase.ICP.Agent.Responses.QueryRejectInfo.Code') - [ErrorCode](#P-EdjCase-ICP-Agent-Responses-QueryRejectInfo-ErrorCode 'EdjCase.ICP.Agent.Responses.QueryRejectInfo.ErrorCode') @@ -206,6 +211,7 @@ - [Sender](#P-EdjCase-ICP-Agent-Requests-QueryRequest-Sender 'EdjCase.ICP.Agent.Requests.QueryRequest.Sender') - [BuildHashableItem()](#M-EdjCase-ICP-Agent-Requests-QueryRequest-BuildHashableItem 'EdjCase.ICP.Agent.Requests.QueryRequest.BuildHashableItem') - [QueryResponse](#T-EdjCase-ICP-Agent-Responses-QueryResponse 'EdjCase.ICP.Agent.Responses.QueryResponse') + - [Signatures](#P-EdjCase-ICP-Agent-Responses-QueryResponse-Signatures 'EdjCase.ICP.Agent.Responses.QueryResponse.Signatures') - [Type](#P-EdjCase-ICP-Agent-Responses-QueryResponse-Type 'EdjCase.ICP.Agent.Responses.QueryResponse.Type') - [AsRejected()](#M-EdjCase-ICP-Agent-Responses-QueryResponse-AsRejected 'EdjCase.ICP.Agent.Responses.QueryResponse.AsRejected') - [AsReplied()](#M-EdjCase-ICP-Agent-Responses-QueryResponse-AsReplied 'EdjCase.ICP.Agent.Responses.QueryResponse.AsReplied') @@ -2483,6 +2489,49 @@ Nat value Text value + +## NodeSignature `type` + +##### Namespace + +EdjCase.ICP.Agent.Responses + +##### Summary + +Signature data from a replica node + + +### #ctor(timestamp,signature,identity) `constructor` + +##### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| timestamp | [EdjCase.ICP.Candid.Models.ICTimestamp](#T-EdjCase-ICP-Candid-Models-ICTimestamp 'EdjCase.ICP.Candid.Models.ICTimestamp') | Timestamp when the signature was created | +| signature | [System.Byte[]](http://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k:System.Byte[] 'System.Byte[]') | The signature bytes | +| identity | [EdjCase.ICP.Candid.Models.Principal](#T-EdjCase-ICP-Candid-Models-Principal 'EdjCase.ICP.Candid.Models.Principal') | The identity of the signer | + + +### Identity `property` + +##### Summary + +The identity of the signer + + +### Signature `property` + +##### Summary + +The signature bytes + + +### Timestamp `property` + +##### Summary + +Timestamp when the signature was created + ## QueryRejectInfo `type` @@ -2645,6 +2694,13 @@ EdjCase.ICP.Agent.Responses A model representing the response data in the form of a variant + +### Signatures `property` + +##### Summary + +Signatures from the replica node + ### Type `property` diff --git a/src/Agent/API.xml b/src/Agent/API.xml index d297c7c0..c76e4d72 100644 --- a/src/Agent/API.xml +++ b/src/Agent/API.xml @@ -903,6 +903,31 @@ Optional. A specific error id for the reject + + + Signature data from a replica node + + + + + Timestamp when the signature was created + + + + + The signature bytes + + + + + The identity of the signer + + + + Timestamp when the signature was created + The signature bytes + The identity of the signer + A model representing the response data in the form of a variant @@ -914,6 +939,11 @@ to extract the variant data + + + Signatures from the replica node + + Gets the reply data IF the response type is 'replied'. Otherwise will throw exception diff --git a/src/Agent/Responses/CborUtil.cs b/src/Agent/Responses/CborUtil.cs new file mode 100644 index 00000000..e07e7fb0 --- /dev/null +++ b/src/Agent/Responses/CborUtil.cs @@ -0,0 +1,31 @@ +using EdjCase.ICP.Candid.Encodings; +using EdjCase.ICP.Candid.Models; +using System; +using System.Collections.Generic; +using System.Formats.Cbor; +using System.Linq; +using System.Text; + +namespace EdjCase.ICP.Agent.Responses +{ + internal static class CborUtil + { + public static UnboundedUInt ReadNat(CborReader reader) + { + CborReaderState state = reader.PeekState(); + switch (state) + { + case CborReaderState.UnsignedInteger: + return reader.ReadUInt64(); + default: + byte[] codeBytes = reader.ReadByteString().ToArray(); + return LEB128.DecodeUnsigned(codeBytes); + } + } + + public static Principal ReadPrincipal(CborReader reader) + { + return Principal.FromBytes(reader.ReadByteString()); + } + } +} diff --git a/src/Agent/Responses/NodeSignature.cs b/src/Agent/Responses/NodeSignature.cs new file mode 100644 index 00000000..77e5ecc3 --- /dev/null +++ b/src/Agent/Responses/NodeSignature.cs @@ -0,0 +1,89 @@ +using EdjCase.ICP.Agent.Models; +using EdjCase.ICP.Candid.Models; +using Org.BouncyCastle.Crypto.Agreement; +using System; +using System.Collections.Generic; +using System.Formats.Cbor; +using System.Linq; +using System.Text; + +namespace EdjCase.ICP.Agent.Responses +{ + /// + /// Signature data from a replica node + /// + public class NodeSignature + { + /// + /// Timestamp when the signature was created + /// + public ICTimestamp Timestamp { get; } + /// + /// The signature bytes + /// + public byte[] Signature { get; } + /// + /// The identity of the signer + /// + public Principal Identity { get; } + + /// Timestamp when the signature was created + /// The signature bytes + /// The identity of the signer + public NodeSignature(ICTimestamp timestamp, byte[] signature, Principal identity) + { + this.Timestamp = timestamp ?? throw new ArgumentNullException(nameof(timestamp)); + this.Signature = signature ?? throw new ArgumentNullException(nameof(signature)); + this.Identity = identity ?? throw new ArgumentNullException(nameof(identity)); + } + + internal static NodeSignature ReadCbor(CborReader reader) + { + ICTimestamp? timestamp = null; + byte[]? signature = null; + Principal? identity = null; + _ = reader.ReadStartMap(); + while (reader.PeekState() != CborReaderState.EndMap) + { + string name = reader.ReadTextString(); + switch (name) + { + case "timestamp": + timestamp = ICTimestamp.FromNanoSeconds(CborUtil.ReadNat(reader)); + break; + case "signature": + signature = reader.ReadByteString(); + break; + case "identity": + identity = CborUtil.ReadPrincipal(reader); + break; + default: + // Skip + reader.SkipValue(); + break; + } + } + reader.ReadEndMap(); + + if (timestamp == null) + { + throw new Exception("Node signature is missing the timestamp field"); + } + if (signature == null) + { + throw new Exception("Node signature is missing the signature field"); + } + if (identity == null) + { + throw new Exception("Node signature is missing the identity field"); + } + + + return new NodeSignature( + timestamp, + signature, + identity + ); + } + } +} diff --git a/src/Agent/Responses/QueryResponse.cs b/src/Agent/Responses/QueryResponse.cs index 4cfcc9d8..96a0ef9f 100644 --- a/src/Agent/Responses/QueryResponse.cs +++ b/src/Agent/Responses/QueryResponse.cs @@ -2,7 +2,9 @@ using EdjCase.ICP.Candid.Encodings; using EdjCase.ICP.Candid.Models; using EdjCase.ICP.Candid.Utilities; +using Org.BouncyCastle.Asn1.Ocsp; using System; +using System.Collections.Generic; using System.Formats.Cbor; using System.Linq; using System.Xml.Linq; @@ -19,11 +21,16 @@ public class QueryResponse /// to extract the variant data /// public QueryResponseType Type { get; } + /// + /// Signatures from the replica node + /// + public List Signatures { get; } private readonly object value; - private QueryResponse(QueryResponseType type, object value) + private QueryResponse(QueryResponseType type, List signatures, object value) { this.Type = type; + this.Signatures = signatures; this.value = value; } @@ -49,6 +56,7 @@ public QueryRejectInfo AsRejected() return (QueryRejectInfo)this.value; } + private void ThrowIfWrongType(QueryResponseType type) { if (this.Type != type) @@ -73,14 +81,14 @@ public CandidArg ThrowOrGetReply() return this.AsReplied(); } - internal static QueryResponse Rejected(RejectCode code, string? message, string? errorCode) + internal static QueryResponse Rejected(RejectCode code, List signatures, string? message, string? errorCode) { - return new QueryResponse(QueryResponseType.Rejected, new QueryRejectInfo(code, message, errorCode)); + return new QueryResponse(QueryResponseType.Rejected, signatures, new QueryRejectInfo(code, message, errorCode)); } - internal static QueryResponse Replied(CandidArg reply) + internal static QueryResponse Replied(CandidArg reply, List signatures) { - return new QueryResponse(QueryResponseType.Replied, reply); + return new QueryResponse(QueryResponseType.Replied, signatures, reply); } internal static QueryResponse ReadCbor(CborReader reader) @@ -90,6 +98,7 @@ internal static QueryResponse ReadCbor(CborReader reader) UnboundedUInt? rejectCode = null; string? rejectMessage = null; string? errorCode = null; + List signatures = new (); if (reader.ReadTag() != CborTag.SelfDescribeCbor) { @@ -120,17 +129,7 @@ internal static QueryResponse ReadCbor(CborReader reader) reader.ReadEndMap(); break; case "reject_code": - CborReaderState state = reader.PeekState(); - switch (state) - { - case CborReaderState.UnsignedInteger: - rejectCode = reader.ReadUInt64(); - break; - default: - byte[] codeBytes = reader.ReadByteString().ToArray(); - rejectCode = LEB128.DecodeUnsigned(codeBytes); - break; - } + rejectCode = CborUtil.ReadNat(reader); break; case "reject_message": rejectMessage = reader.ReadTextString(); @@ -138,8 +137,22 @@ internal static QueryResponse ReadCbor(CborReader reader) case "error_code": errorCode = reader.ReadTextString(); break; + case "signatures": + reader.ReadStartArray(); + while (reader.PeekState() != CborReaderState.EndArray) + { + signatures.Add(NodeSignature.ReadCbor(reader)); + } + reader.ReadEndArray(); + break; default: +#if DEBUG throw new NotImplementedException($"Cannot deserialize query response. Unknown field '{name}'"); +#else + reader.SkipValue(); + break; +#endif + } } reader.ReadEndMap(); @@ -160,14 +173,14 @@ internal static QueryResponse ReadCbor(CborReader reader) string argHex = ByteUtil.ToHexString(replyArg!); #endif var arg = CandidArg.FromBytes(replyArg!); - return QueryResponse.Replied(arg); + return QueryResponse.Replied(arg, signatures); case "rejected": if (rejectCode == null) { throw new CborContentException("Missing field: reject_code"); } RejectCode code = (RejectCode)(ulong)rejectCode!; - return QueryResponse.Rejected(code, rejectMessage, errorCode); + return QueryResponse.Rejected(code, signatures, rejectMessage, errorCode); default: throw new NotImplementedException($"Cannot deserialize query response with status '{status}'"); }