From d42b9a38062f3f2db5c088e06a76833482f6f63b Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Fri, 17 Jan 2025 15:15:07 +0700 Subject: [PATCH 01/18] feat(docs): Enhance the catalyst rbac URI so it can be used as a general identifier as well as a key identifier --- .../{rbac_kid_uri => rbac_id_uri}/.pages | 2 +- .../catalyst-id-uri.md} | 95 +++++++++++++------ 2 files changed, 68 insertions(+), 29 deletions(-) rename docs/src/architecture/08_concepts/{rbac_kid_uri => rbac_id_uri}/.pages (66%) rename docs/src/architecture/08_concepts/{rbac_kid_uri/kiduri.md => rbac_id_uri/catalyst-id-uri.md} (62%) diff --git a/docs/src/architecture/08_concepts/rbac_kid_uri/.pages b/docs/src/architecture/08_concepts/rbac_id_uri/.pages similarity index 66% rename from docs/src/architecture/08_concepts/rbac_kid_uri/.pages rename to docs/src/architecture/08_concepts/rbac_id_uri/.pages index 0bcc138749..35d55f1a31 100644 --- a/docs/src/architecture/08_concepts/rbac_kid_uri/.pages +++ b/docs/src/architecture/08_concepts/rbac_id_uri/.pages @@ -1,3 +1,3 @@ title: RBAC KID (Key Identifier) URI arrange: - - kiduri.md + - catalyst-id-uri.md diff --git a/docs/src/architecture/08_concepts/rbac_kid_uri/kiduri.md b/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md similarity index 62% rename from docs/src/architecture/08_concepts/rbac_kid_uri/kiduri.md rename to docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md index fd09c68470..1266d1672c 100644 --- a/docs/src/architecture/08_concepts/rbac_kid_uri/kiduri.md +++ b/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md @@ -1,5 +1,5 @@ --- -Title: RBAC Key Identifier URI Specification +Title: RBAC Catalyst Identifier URI Specification Category: Catalyst Status: Proposed Authors: @@ -20,8 +20,7 @@ License: CC-BY-4.0 * [`authority` - `host`](#authority---host) * [List of defined hosts](#list-of-defined-hosts) * [`authority` - `userinfo`](#authority---userinfo) - * [Lists of defined subnetwork `userinfo` values](#lists-of-defined-subnetwork-userinfo-values) - * [Cardano](#cardano) + * [Example `userinfo` with a `hostname`](#example-userinfo-with-a-hostname) * [`path`](#path) * [Reference Implementation](#reference-implementation) * [Test Vectors](#test-vectors) @@ -38,34 +37,37 @@ unambiguously identified. ## Motivation: why is this CIP necessary? -There is a need to identify which Key from a RBAC registration was used to sign data. +There is a need to identify which RBAC Registration is referenced, +or which Key from a RBAC registration was used to sign data. RBAC defines a universal keychain of different keys that can be used for different purposes. They can be used not only for Signatures, but also Encryption. -Therefore, there needs to be an unambiguous and easy to lookup identifier to signify which key was -used for a particular purpose. +Sometimes all that is required is to identify the individual key chain. +Other times a specific key on the chain needs to be referenced. -This document defines a [URI] scheme to unambiguously define a particular key with reference to a -particular RBAC keychain. +Therefore, there needs to be an unambiguous and easy to lookup identifier to signify which keychain, +or key in a particular chain was used for a particular purpose. + +This document defines a [URI] scheme to unambiguously define a keychain or a specific key within the keychain. ## Specification ### URI -The RBAC Kid is formatted using a [Universal Resource Identifier]. +The Catalyst RBAC Id is formatted using a [Universal Resource Identifier]. Refer to [RFC3986] for the specification of the URI format. ### `scheme` -The [scheme](https://datatracker.ietf.org/doc/html/rfc3986#section-3.1) **MUST** be `kid.catalyst-rbac`; +The [scheme](https://datatracker.ietf.org/doc/html/rfc3986#section-3.1) **MUST** be `id.catalyst`; ### `authority` The [authority](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2) references the blockchain or network the key was registered within. -It is perfectly valid for a Kid to reference a different network than the place where the Key is used. -For example, a `cardano` KID can be used to post documents to `IPFS`. +It is perfectly valid for a Kid to reference a different network than the place where the Id or Key is used. +For example, a `cardano` ID can be used to post documents to `IPFS`. Its purpose is to define WHERE the key was registered, and nothing more. The Authority will consist of a `host` and optional `userinfo`. @@ -78,30 +80,43 @@ It **IS NOT** resolvable with **DNS**, and **IS NOT** a public host name. It is used as a decentralized network identifier. The consumer of the `KID` must be able to resolve these host names. +The hostname may have one or more subdomains which could specify side-chains of a particular network, +or test networks. + ##### List of defined hosts | `host` | Description | | --- | --- | | `cardano` | Cardano Blockchain | +| `preprod.cardano` | Preprod Cardano Blockchain test network | +| `preview.cardano` | Preview Cardano Blockchain test network | | `midnight` | Midnight Blockchain | | `ethereum` | Ethereum Blockchain | | `cosmos` | Cosmos Blockchain | +This list is indicative of the host names that can be used, any hostname is valid provided it is +capable of storing catalyst RBAC registration keychains. + #### `authority` - `userinfo` -The [userinfo](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1) -is used to distinguish a subnetwork from the primary main network. -The absence of `userinfo` is used to indicate the primary main network. +The [userinfo] is used to hold a user defined readable name that can be attached to the keychain. +It may contain an optional `nonce` which is separated from the users name by a `:` and replaces a +traditional password used for HTTP basic authentication. -##### Lists of defined subnetwork `userinfo` values +Because the name is not unique, and is provided by the user, it is informational only. +A URI is identical, provided the hostname and path are the same, the [userinfo] does not play +a part in validating or finding the catalyst keychain being referenced. -###### Cardano +The `nonce` part contained in the `password` component of the username *MUST* be an integer, +and it is the number of seconds since 1970 UTC, when the nonce was generated. -| `userinfo` | Description | -| --- | --- | -| `preprod` | Cardano Pre-Production Network | -| `preview` | Cardano Preview Network | -| 0x | Cardano network identified by this magic number in hex | +Applications which use the `nonce` will define its use, anything that does not use the `nonce` will ignore it. + +##### Example `userinfo` with a `hostname` + +* `anne@cardano` - username `anne` no nonce. +* `blake:1737101079@midnight` - username `blake` with nonce 1737101079. +* `:173710179#ethereum` - no username with nonce 173710179. ### `path` @@ -115,13 +130,16 @@ The overall `path` specification is: `//#encr * This does not change, even if the Initial Role 0 key is revoked. * This allows for an unambiguous identifier for the RBAC keychain. * It is not necessarily the key being identified. -* `` - This is the Role number being used. +* `` - *Optional* This is the Role number being used. * It is a positive number, starting at 0, and no greater than 65535. -* `` - This is the rotation of the defined role key being identified. + * If it is not defined, then its default value is 0. + * If it is not defined, there can be no `` part of the path following. +* `` - *Optional* This is the rotation of the defined role key being identified. * It starts at 0 for the first published key for the role, and increments by one for each subsequent published rotation. * This number refers to the published sequence of keys for the role in the RBAC registration keychain, not the index used in the key derivation. * It is positive and no greater than 65535. + * If not present, it defaults to 0. * `#encrypt` - [Fragment](https://datatracker.ietf.org/doc/html/rfc3986#section-3.5) disambiguates Encryption Public Keys from signing public keys. * Roles can have 1 active public signing key, and 1 active public encryption key. @@ -134,26 +152,46 @@ The first implementation will be Catalyst Voices. ## Test Vectors -* `kid.catalyst-rbac://cardano//0/0` +* `id.catalyst://cardano/` * A Signing key registered on the Cardano Main network. * Role 0 - Rotation 0. + * `username` - undefined. + * `nonce` - undefined. In this example, it is exactly the same as the ``. -* `kid.catalyst-rbac://preprod@cardano//7/3` +* `id.catalyst://cardano//0` + * A Signing key registered on the Cardano Main network. + * Role 0 - Rotation 0. + * `username` - undefined. + * `nonce` - undefined. +* `id.catalyst://gary@cardano//0/0` + * A Signing key registered on the Cardano Main network. + * Role 0 - Rotation 0. + * `username` - `gary`. + * `nonce` - undefined. +* `id.catalyst://faith@preprod@cardano//7/3` * A Signing key registered on the Cardano pre-production network. * Role 7 - Rotation 3. + * `username` - `faith` + * `nonce` - undefined. The Key for Role 7, and its third published rotation (i.e., the fourth key published, the first is the initial key, plus 3 rotations following it). -* `kid.catalyst-rbac://preprod@cardano//2/0#encrypt` +* `id.catalyst://faith:173710179@preprod@cardano//2/0#encrypt` * A Public Encryption key registered on the Cardano pre-production network. * Role 2 - Rotation 0. + * `username` - `faith` + * `nonce` - 173710179. The initially published Public Encryption Key for Role 2. -* `kid.catalyst-rbac://midnight//0/1` +* `kid.catalyst-rbac://:173710179@midnight//0/1` * A Signing key registered on the Midnight Blockchain Main network * Role 0 - Rotation 1. + * `username` - undefined. + * `nonce` - 173710179. In this example, it is NOT the same as the ``, as it identifies the first rotation after ``. * `kid.catalyst-rbac://midnight//2/1#encrypt` * A public encryption key registered on the Midnight Blockchain Main network. * Role 2 - Rotation 1. + * `username` - undefined. + * `nonce` - 173710179. ## Rationale: how does this CIP achieve its goals? @@ -178,3 +216,4 @@ This document is licensed under [CC-BY-4.0](https://creativecommons.org/licenses [Universal Resource Identifier]: https://datatracker.ietf.org/doc/html/rfc3986 [RFC3986]: https://datatracker.ietf.org/doc/html/rfc3986 [Base64 URL]: https://datatracker.ietf.org/doc/html/rfc4648#section-5 +[userinfo]: (https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1) From e992aee7bfe09de8c7eb8763d1b6afcdb7846f10 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Fri, 17 Jan 2025 15:19:47 +0700 Subject: [PATCH 02/18] fix(rust): rename the kid_uri to just an id_uri to better reflect its type --- rust/catalyst-types/src/{kid_uri => id_uri}/errors.rs | 0 rust/catalyst-types/src/{kid_uri => id_uri}/key_rotation.rs | 0 rust/catalyst-types/src/{kid_uri => id_uri}/mod.rs | 0 rust/catalyst-types/src/{kid_uri => id_uri}/role_index.rs | 0 rust/catalyst-types/src/lib.rs | 2 +- rust/catalyst-types/tests/mod.rs | 2 +- 6 files changed, 2 insertions(+), 2 deletions(-) rename rust/catalyst-types/src/{kid_uri => id_uri}/errors.rs (100%) rename rust/catalyst-types/src/{kid_uri => id_uri}/key_rotation.rs (100%) rename rust/catalyst-types/src/{kid_uri => id_uri}/mod.rs (100%) rename rust/catalyst-types/src/{kid_uri => id_uri}/role_index.rs (100%) diff --git a/rust/catalyst-types/src/kid_uri/errors.rs b/rust/catalyst-types/src/id_uri/errors.rs similarity index 100% rename from rust/catalyst-types/src/kid_uri/errors.rs rename to rust/catalyst-types/src/id_uri/errors.rs diff --git a/rust/catalyst-types/src/kid_uri/key_rotation.rs b/rust/catalyst-types/src/id_uri/key_rotation.rs similarity index 100% rename from rust/catalyst-types/src/kid_uri/key_rotation.rs rename to rust/catalyst-types/src/id_uri/key_rotation.rs diff --git a/rust/catalyst-types/src/kid_uri/mod.rs b/rust/catalyst-types/src/id_uri/mod.rs similarity index 100% rename from rust/catalyst-types/src/kid_uri/mod.rs rename to rust/catalyst-types/src/id_uri/mod.rs diff --git a/rust/catalyst-types/src/kid_uri/role_index.rs b/rust/catalyst-types/src/id_uri/role_index.rs similarity index 100% rename from rust/catalyst-types/src/kid_uri/role_index.rs rename to rust/catalyst-types/src/id_uri/role_index.rs diff --git a/rust/catalyst-types/src/lib.rs b/rust/catalyst-types/src/lib.rs index 57cdd356a1..8cbf1e7d92 100644 --- a/rust/catalyst-types/src/lib.rs +++ b/rust/catalyst-types/src/lib.rs @@ -2,6 +2,6 @@ pub mod conversion; pub mod hashes; -pub mod kid_uri; +pub mod id_uri; pub mod problem_report; pub mod uuid; diff --git a/rust/catalyst-types/tests/mod.rs b/rust/catalyst-types/tests/mod.rs index 0735deef50..e8d5156342 100644 --- a/rust/catalyst-types/tests/mod.rs +++ b/rust/catalyst-types/tests/mod.rs @@ -12,7 +12,7 @@ fn test_type_usage() { type D = catalyst_types::uuid::V4; type E = catalyst_types::uuid::V7; - type F = catalyst_types::kid_uri::KidUri; + type F = catalyst_types::id_uri::KidUri; let bytes: [u8; 32] = [0; 32]; let _ = catalyst_types::hashes::Blake2bHash::from(bytes); From 5bad43a0ab985fe98c1e6da9a92db83b30f97a71 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Fri, 17 Jan 2025 15:42:39 +0700 Subject: [PATCH 03/18] fix(rust): remove cruft --- rust/cardano-blockchain-types/deps.tmp | 61 -------------------------- 1 file changed, 61 deletions(-) delete mode 100644 rust/cardano-blockchain-types/deps.tmp diff --git a/rust/cardano-blockchain-types/deps.tmp b/rust/cardano-blockchain-types/deps.tmp deleted file mode 100644 index f65f5bfb55..0000000000 --- a/rust/cardano-blockchain-types/deps.tmp +++ /dev/null @@ -1,61 +0,0 @@ -rbac-registration = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.8" } - -thiserror = "1.0.64" -tokio = { version = "1.40.0", features = [ - "macros", - "rt", - "net", - "rt-multi-thread", -] } -tracing = "0.1.40" -tracing-log = "0.2.0" -dashmap = "6.1.0" -url = "2.5.2" -anyhow = "1.0.89" -chrono = "0.4.38" -async-trait = "0.1.83" -dirs = "5.0.1" -futures = "0.3.31" -humantime = "2.1.0" -crossbeam-skiplist = "0.1.3" -crossbeam-channel = "0.5.13" -crossbeam-epoch = "0.9.18" -strum = "0.26.3" -ouroboros = "0.18.4" -hex = "0.4.3" -rayon = "1.10.0" -serde = "1.0.210" -serde_json = "1.0.128" -mimalloc = { version = "0.1.43", optional = true } -memx = "0.1.32" -fmmap = { version = "0.3.3", features = ["sync", "tokio-async"] } -minicbor = { version = "0.25.1", features = ["alloc", "derive", "half"] } -zstd = "0.13.2" -ed25519-dalek = "2.1.1" -blake2b_simd = "1.0.2" -num-traits = "0.2.19" -logcall = "0.1.9" -tar = "0.4.42" -ureq = { version = "2.10.1", features = ["native-certs"] } -http = "1.1.0" -hickory-resolver = { version = "0.24.1", features = ["dns-over-rustls"] } -moka = { version = "0.12.8", features = ["sync"] } - -hex = "0.4.3" -anyhow = "1.0.89" -strum_macros = "0.26.4" -regex = "1.11.0" -minicbor = { version = "0.25.1", features = ["alloc", "derive", "half"] } -brotli = "7.0.0" -zstd = "0.13.2" -x509-cert = "0.2.5" -der-parser = "9.0.0" -bech32 = "0.11.0" -dashmap = "6.1.0" -blake2b_simd = "1.0.2" -tracing = "0.1.40" -ed25519-dalek = "2.1.1" -uuid = "1.11.0" - -c509-certificate = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git" , tag = "v0.0.3" } -pallas = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } From 42a4a996b6a7596b124bcd14a777c9a44ecea23d Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Sun, 19 Jan 2025 15:06:34 +0700 Subject: [PATCH 04/18] feat(rust): New catalyst IdUri type to replace the more limited Kid --- .../rbac_id_uri/catalyst-id-uri.md | 28 +- rust/catalyst-types/Cargo.toml | 5 +- rust/catalyst-types/src/id_uri/errors.rs | 8 +- .../catalyst-types/src/id_uri/key_rotation.rs | 17 + rust/catalyst-types/src/id_uri/mod.rs | 427 +++++++++++++++--- rust/catalyst-types/src/id_uri/role_index.rs | 17 + rust/catalyst-types/tests/mod.rs | 2 +- 7 files changed, 422 insertions(+), 82 deletions(-) diff --git a/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md b/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md index 1266d1672c..64ade93b92 100644 --- a/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md +++ b/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md @@ -32,7 +32,7 @@ License: CC-BY-4.0 ## Abstract -Definition of a [URI] which allows for RBAC keys used for different purposes to be easily and +Definition of a [URI], which allows for RBAC keys used for different purposes to be easily and unambiguously identified. ## Motivation: why is this CIP necessary? @@ -42,7 +42,7 @@ or which Key from a RBAC registration was used to sign data. RBAC defines a universal keychain of different keys that can be used for different purposes. They can be used not only for Signatures, but also Encryption. -Sometimes all that is required is to identify the individual key chain. +Sometimes all that is required is to identify the individual keychain. Other times a specific key on the chain needs to be referenced. Therefore, there needs to be an unambiguous and easy to lookup identifier to signify which keychain, @@ -54,19 +54,21 @@ This document defines a [URI] scheme to unambiguously define a keychain or a spe ### URI -The Catalyst RBAC Id is formatted using a [Universal Resource Identifier]. +The Catalyst RBAC ID is formatted using a [Universal Resource Identifier]. Refer to [RFC3986] for the specification of the URI format. ### `scheme` -The [scheme](https://datatracker.ietf.org/doc/html/rfc3986#section-3.1) **MUST** be `id.catalyst`; +The [scheme](https://datatracker.ietf.org/doc/html/rfc3986#section-3.1) **MUST** be `id.catalyst`. + +When used as a Catalyst ID, where only catalyst IDs would be used, the scheme can be omitted. ### `authority` The [authority](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2) references the blockchain or network the key was registered within. -It is perfectly valid for a Kid to reference a different network than the place where the Id or Key is used. +It is perfectly valid for an ID Uri to reference a different network than the place where the ID or Key is used. For example, a `cardano` ID can be used to post documents to `IPFS`. Its purpose is to define WHERE the key was registered, and nothing more. @@ -100,7 +102,7 @@ capable of storing catalyst RBAC registration keychains. #### `authority` - `userinfo` The [userinfo] is used to hold a user defined readable name that can be attached to the keychain. -It may contain an optional `nonce` which is separated from the users name by a `:` and replaces a +It may contain an optional `nonce` which is separated from the user's name by a `:` and replaces a traditional password used for HTTP basic authentication. Because the name is not unique, and is provided by the user, it is informational only. @@ -108,13 +110,13 @@ A URI is identical, provided the hostname and path are the same, the [userinfo] a part in validating or finding the catalyst keychain being referenced. The `nonce` part contained in the `password` component of the username *MUST* be an integer, -and it is the number of seconds since 1970 UTC, when the nonce was generated. +and it is the number of seconds since 1970 UTC, when the Catalyst ID URI was generated. Applications which use the `nonce` will define its use, anything that does not use the `nonce` will ignore it. ##### Example `userinfo` with a `hostname` -* `anne@cardano` - username `anne` no nonce. +* `anne@cardano` - username `anne` no nonce. * `blake:1737101079@midnight` - username `blake` with nonce 1737101079. * `:173710179#ethereum` - no username with nonce 173710179. @@ -157,36 +159,38 @@ The first implementation will be Catalyst Voices. * Role 0 - Rotation 0. * `username` - undefined. * `nonce` - undefined. - In this example, it is exactly the same as the ``. + * In this example, it is identical to `/0/0` or `/0`. * `id.catalyst://cardano//0` * A Signing key registered on the Cardano Main network. * Role 0 - Rotation 0. * `username` - undefined. * `nonce` - undefined. + * In this example, it is identical to `/0/0` or ``. * `id.catalyst://gary@cardano//0/0` * A Signing key registered on the Cardano Main network. * Role 0 - Rotation 0. * `username` - `gary`. * `nonce` - undefined. + * In this example, it is identical to `` or `/0`. * `id.catalyst://faith@preprod@cardano//7/3` * A Signing key registered on the Cardano pre-production network. * Role 7 - Rotation 3. * `username` - `faith` * `nonce` - undefined. - The Key for Role 7, and its third published rotation + * The Key for Role 7, and its third published rotation (i.e., the fourth key published, the first is the initial key, plus 3 rotations following it). * `id.catalyst://faith:173710179@preprod@cardano//2/0#encrypt` * A Public Encryption key registered on the Cardano pre-production network. * Role 2 - Rotation 0. * `username` - `faith` * `nonce` - 173710179. - The initially published Public Encryption Key for Role 2. + * The initially published Public Encryption Key for Role 2. * `kid.catalyst-rbac://:173710179@midnight//0/1` * A Signing key registered on the Midnight Blockchain Main network * Role 0 - Rotation 1. * `username` - undefined. * `nonce` - 173710179. - In this example, it is NOT the same as the ``, as it identifies the first rotation after ``. + * In this example, it is NOT the same as the ``, as it identifies the first rotation after ``. * `kid.catalyst-rbac://midnight//2/1#encrypt` * A public encryption key registered on the Midnight Blockchain Main network. * Role 2 - Rotation 1. diff --git a/rust/catalyst-types/Cargo.toml b/rust/catalyst-types/Cargo.toml index 52e3045125..29357946ba 100644 --- a/rust/catalyst-types/Cargo.toml +++ b/rust/catalyst-types/Cargo.toml @@ -26,9 +26,10 @@ num-traits = "0.2.19" orx-concurrent-vec = "3.1.0" pallas-crypto = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } serde = { version = "1.0.217", features = ["derive"] } -thiserror = "2.0.9" +thiserror = "2.0.11" base64-url = "3.0.0" -uuid = { version = "1.11.0", features = ["v4", "v7", "serde"] } +uuid = { version = "1.12.0", features = ["v4", "v7", "serde"] } +chrono = "0.4.39" [dev-dependencies] ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } diff --git a/rust/catalyst-types/src/id_uri/errors.rs b/rust/catalyst-types/src/id_uri/errors.rs index 7730c6a1a4..eb8e31732f 100644 --- a/rust/catalyst-types/src/id_uri/errors.rs +++ b/rust/catalyst-types/src/id_uri/errors.rs @@ -7,13 +7,15 @@ use super::{key_rotation::KeyRotationError, role_index::RoleIndexError}; /// Errors that can occur when parsing a `KidUri` #[derive(Display, Error, Debug)] -pub enum KidUriError { +pub enum IdUriError { /// Invalid KID URI - InvalidURI(#[from] fluent_uri::error::ParseError), - /// Invalid Scheme, not a KID URI + InvalidURI(#[from] fluent_uri::error::ParseError), + /// Invalid Scheme, not a ID URI InvalidScheme, /// Network not defined in URI NoDefinedNetwork, + /// Invalid Nonce + InvalidNonce, /// Path of URI is invalid InvalidPath, /// Role 0 Key in path is invalid diff --git a/rust/catalyst-types/src/id_uri/key_rotation.rs b/rust/catalyst-types/src/id_uri/key_rotation.rs index 48b262f2bd..02ea2ae404 100644 --- a/rust/catalyst-types/src/id_uri/key_rotation.rs +++ b/rust/catalyst-types/src/id_uri/key_rotation.rs @@ -21,6 +21,23 @@ pub enum KeyRotationError { #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct KeyRotation(u16); +impl KeyRotation { + /// Default Role Index + pub const DEFAULT: KeyRotation = KeyRotation(0); + + /// Is the `KeyRotation` the default value + #[must_use] + pub fn is_default(self) -> bool { + self == Self::DEFAULT + } +} + +impl Default for KeyRotation { + fn default() -> Self { + Self::DEFAULT + } +} + impl From for KeyRotation { fn from(value: u16) -> Self { Self(value) diff --git a/rust/catalyst-types/src/id_uri/mod.rs b/rust/catalyst-types/src/id_uri/mod.rs index d58dbac92f..14f9f8235b 100644 --- a/rust/catalyst-types/src/id_uri/mod.rs +++ b/rust/catalyst-types/src/id_uri/mod.rs @@ -11,6 +11,7 @@ use std::{ str::FromStr, }; +use chrono::{DateTime, Duration, Utc}; use ed25519_dalek::VerifyingKey; use fluent_uri::{ component::Scheme, @@ -23,13 +24,17 @@ use fluent_uri::{ use key_rotation::KeyRotation; use role_index::RoleIndex; -/// Catalyst Signed Document Key ID +/// Catalyst ID /// -/// Key ID associated with a `COSE` Signature that is structured as a Universal Resource -/// Identifier (`URI`). -#[derive(Debug, Clone)] +/// Identity of Catalyst Registration. +/// Optionally also identifies a specific Signed Document Key +#[derive(Debug, Clone, PartialEq, Hash)] #[allow(clippy::module_name_repetitions)] -pub struct KidUri { +pub struct IdUri { + /// Username + username: Option, + /// Nonce (like the password in http basic auth, but NOT a password, just a nonce) + nonce: Option>, /// Network network: String, /// Sub Network @@ -44,15 +49,38 @@ pub struct KidUri { /// - `true`: The key is used for encryption. /// - `false`: The key is used for signing (signature key). encryption: bool, + /// Indicates if this is an `id` type, or a `uri` type. + /// Used by the serialization functions. + /// `true` = format as an `Id` + /// `false` = format as a `Uri` + id: bool, } -impl KidUri { +impl IdUri { /// Encryption Key Identifier Fragment const ENCRYPTION_FRAGMENT: &EStr = EStr::new_or_panic("encrypt"); + /// Maximum allowable Nonce Value + /// * Monday, January 1, 2125 12:00:00 AM + const MAX_NONCE: i64 = 4_891_363_200; + /// Minimum allowable Nonce Value + /// * Wednesday, January 1, 2025 12:00:00 AM + const MIN_NONCE: i64 = 1_735_689_600; /// URI scheme for Catalyst - const SCHEME: &Scheme = Scheme::new_or_panic("kid.catalyst-rbac"); + const SCHEME: &Scheme = Scheme::new_or_panic("id.catalyst"); - /// Get the network the `KidUri` is referencing the registration to. + /// Get the cosmetic username from the URI. + #[must_use] + pub fn username(&self) -> Option { + self.username.clone() + } + + /// Get the nonce from the URI. + #[must_use] + pub fn nonce(&self) -> Option> { + self.nonce + } + + /// Get the network the `IdUri` is referencing the registration to. #[must_use] pub fn network(&self) -> (String, Option) { (self.network.clone(), self.subnet.clone()) @@ -81,108 +109,362 @@ impl KidUri { pub fn role_and_rotation(&self) -> (RoleIndex, KeyRotation) { (self.role, self.rotation) } -} -impl KidUri { - /// Create a new `KidUri` for a Signing Key - fn new( + /// Create a new `IdUri` for a Signing Key + #[must_use] + pub fn new( network: &str, subnet: Option<&str>, role0_pk: VerifyingKey, role: RoleIndex, rotation: KeyRotation, ) -> Self { Self { + username: None, // Default to Not set, use `with_username` if required. + nonce: None, // Default to Not set, use `with_nonce` if required. network: network.to_string(), subnet: subnet.map(str::to_string), role0_pk, role, rotation, encryption: false, + id: false, // Default to `URI` formatted. } } - /// Create a new `KidUri` for an Encryption Key - fn new_encryption( - network: &str, subnet: Option<&str>, role0_pk: VerifyingKey, role: RoleIndex, - rotation: KeyRotation, - ) -> Self { - let mut kid = Self::new(network, subnet, role0_pk, role, rotation); - kid.encryption = true; - kid + /// The `IdUri` is formatted as a URI. + #[must_use] + pub fn as_uri(self) -> Self { + Self { id: false, ..self } + } + + /// The `IdUri` is formatted as a id. + #[must_use] + pub fn as_id(self) -> Self { + Self { id: true, ..self } + } + + /// Was `IdUri` formatted as a id when it was parsed. + #[must_use] + pub fn is_id(self) -> bool { + self.id + } + + /// Add or change the username in a Catalyst ID URI. + #[must_use] + pub fn with_username(self, name: &str) -> Self { + Self { + username: Some(name.to_string()), + ..self + } + } + + /// Add or change the username in a Catalyst ID URI. + #[must_use] + pub fn without_username(self) -> Self { + Self { + username: None, + ..self + } + } + + /// Add or change the nonce to a specific value in a Catalyst ID URI. + #[must_use] + pub fn with_specific_nonce(self, nonce: DateTime) -> Self { + Self { + nonce: Some(nonce), + ..self + } + } + + /// Add or change the nonce in a Catalyst ID URI. + #[must_use] + pub fn with_nonce(self) -> Self { + self.with_specific_nonce(Utc::now()) + } + + /// Set that there is no Nonce in the ID or URI + #[must_use] + pub fn without_nonce(self) -> Self { + Self { + nonce: None, + ..self + } + } + + /// Create a new `IdUri` for an Encryption Key + #[must_use] + pub fn with_encryption(self) -> Self { + Self { + encryption: true, + ..self + } + } + + /// Set that the ID is not for encryption + #[must_use] + pub fn without_encryption(self) -> Self { + Self { + encryption: false, + ..self + } + } + + /// Set the role explicitly + #[must_use] + pub fn with_role(self, role: RoleIndex) -> Self { + Self { role, ..self } + } + + /// Set the rotation explicitly + #[must_use] + pub fn with_rotation(self, rotation: KeyRotation) -> Self { + Self { rotation, ..self } + } + + /// Check if the URI has a nonce that falls within the defined boundary around `now()` + /// + /// This function checks whether the nonce (if present) is within the specified time + /// range relative to the current system time (`now`). The range is determined by + /// adding and subtracting the given durations (`past` and `future`) from the current + /// time. If a URI does not have a defined nonce, this function will always return + /// `false`. + /// + /// # Arguments + /// + /// * `self`: A reference to the URI object that contains the potential nonce. + /// * `past`: The duration by which we look back in time from the current moment + /// (`now`). (Positive Duration) + /// * `future`: The duration by which we look forward in time from the current moment + /// (`now`). (Positive Duration) + /// + /// # Returns + /// + /// A boolean value: + /// - `true` if the nonce is within the specified range relative to `now()`. + /// - `false` if there is no nonce defined or if the nonce falls outside the specified + /// range. + /// + /// If the URI does not have a nonce defined, this function returns `false` + /// immediately because it cannot perform the check without a valid nonce present. + /// This behavior ensures that the absence of a nonce will fail any required range + /// checks when such checks are expected according to the function's contract. + /// + /// # Examples + /// + /// ``` + /// use catalyst_types::id_uri::IdUri; + /// use chrono::{DateTime, Duration, Utc}; + /// let uri = "id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE" + /// .parse::() + /// .unwrap() + /// .with_nonce(); + /// // true, within range + /// assert!(uri.is_nonce_in_range(chrono::Duration::hours(1), chrono::Duration::minutes(5))); + /// + /// // Change the nonce to be 1970/1/1 00:00:00 + /// let uri = uri.with_specific_nonce(DateTime::::MIN_UTC); + /// // false, outside range + /// assert!(!uri.is_nonce_in_range(chrono::Duration::hours(1), chrono::Duration::minutes(5))); + /// ``` + #[must_use] + pub fn is_nonce_in_range(&self, past: Duration, future: Duration) -> bool { + if let Some(nonce) = self.nonce { + let now = Utc::now(); + let start_time = now - past; + let end_time = now + future; + (start_time..=end_time).contains(&nonce) + } else { + // No nonce defined, so we say that this fails. + // Prevents an absent Nonce from passing range checks when its required. + false + } + } + + /// Convert the `IdUri` to its shortest form. + /// This is an ID without any role/rotation information, no scheme, no username or + /// nonce. + /// This is used to get the most generalized form of a Catalyst ID. + #[must_use] + pub fn as_short_id(&self) -> Self { + self.clone() + .with_role(RoleIndex::default()) + .with_rotation(KeyRotation::default()) + .without_username() + .without_nonce() + .without_encryption() + .as_id() } } -impl FromStr for KidUri { - type Err = errors::KidUriError; +impl FromStr for IdUri { + type Err = errors::IdUriError; + /// This will parse a URI or a RAW ID. + /// The only difference between them is a URI has the scheme, a raw ID does not. fn from_str(s: &str) -> Result { - let uri = Uri::parse(s)?; + // Did we serialize an ID? + let mut id = false; + + // Check if we have a scheme, and if not default it to the catalyst ID scheme. + let raw_uri = { + if s.contains("://") { + s.to_owned() + } else { + id = true; + // It might be a RAW ID, so try and parse with the correct scheme. + format!("{}://{}", IdUri::SCHEME, s) + } + }; + + let uri = Uri::parse(raw_uri)?; // Check if its the correct scheme. - if uri.scheme() != KidUri::SCHEME { - return Err(errors::KidUriError::InvalidScheme); + if uri.scheme() != IdUri::SCHEME { + return Err(errors::IdUriError::InvalidScheme); } // Decode the network and subnet let auth = uri .authority() - .ok_or(errors::KidUriError::NoDefinedNetwork)?; - let network = auth.host(); - let subnet = auth.userinfo().map(std::string::ToString::to_string); + .ok_or(errors::IdUriError::NoDefinedNetwork)?; + let (subnet, network) = { + let host = auth.host(); + if let Some((subnet, host)) = host.rsplit_once('.') { + (Some(subnet), host) + } else { + (None, host) + } + }; + + let (username, nonce) = { + if let Some(userinfo) = auth.userinfo() { + if let Some((username, nonce)) = userinfo.split_once(':') { + let username = username.decode().into_string_lossy().to_string(); + let nonce_str = nonce.decode().into_string_lossy().to_string(); + + let nonce_val: i64 = nonce_str + .parse() + .map_err(|_| errors::IdUriError::InvalidNonce)?; + if !(IdUri::MIN_NONCE..=IdUri::MAX_NONCE).contains(&nonce_val) { + return Err(errors::IdUriError::InvalidNonce); + } + + let nonce = DateTime::::from_timestamp(nonce_val, 0); + + (Some(username), nonce) + } else { + let username = userinfo.decode().into_string_lossy().to_string(); + (Some(username), None) + } + } else { + (None, None) + } + }; let path: Vec<&EStr> = uri.path().split('/').collect(); // Can ONLY have 3 path components, no more and no less // Less than 3 handled by errors below (4 because of leading `/` in path). if path.len() > 4 { - return Err(errors::KidUriError::InvalidPath); + return Err(errors::IdUriError::InvalidPath); }; // Decode and validate the Role0 Public key from the path - let encoded_role0_key = path.get(1).ok_or(errors::KidUriError::InvalidRole0Key)?; + let encoded_role0_key = path.get(1).ok_or(errors::IdUriError::InvalidRole0Key)?; let decoded_role0_key = base64_url::decode(encoded_role0_key.decode().into_string_lossy().as_ref())?; let role0_pk = crate::conversion::vkey_from_bytes(&decoded_role0_key) - .or(Err(errors::KidUriError::InvalidRole0Key))?; + .or(Err(errors::IdUriError::InvalidRole0Key))?; // Decode and validate the Role Index from the path. - let encoded_role_index = path.get(2).ok_or(errors::KidUriError::InvalidRole)?; - let decoded_role_index = encoded_role_index.decode().into_string_lossy(); - let role_index = decoded_role_index.parse::()?; + let role_index: RoleIndex = { + if let Some(encoded_role_index) = path.get(2) { + let decoded_role_index = encoded_role_index.decode().into_string_lossy(); + decoded_role_index.parse::()? + } else { + RoleIndex::default() + } + }; // Decode and validate the Rotation Value from the path. - let encoded_rotation = path.get(3).ok_or(errors::KidUriError::InvalidRotation)?; - let decoded_rotation = encoded_rotation.decode().into_string_lossy(); - let rotation = decoded_rotation.parse::()?; + let rotation: KeyRotation = { + if let Some(encoded_rotation) = path.get(3) { + let decoded_rotation = encoded_rotation.decode().into_string_lossy(); + decoded_rotation.parse::()? + } else { + KeyRotation::default() + } + }; - let kid = { + let cat_id = { + let mut cat_id = Self::new(network, subnet, role0_pk, role_index, rotation); if uri.has_fragment() { if uri.fragment() == Some(Self::ENCRYPTION_FRAGMENT) { - Self::new_encryption(network, subnet.as_deref(), role0_pk, role_index, rotation) + cat_id = cat_id.with_encryption(); } else { - return Err(errors::KidUriError::InvalidEncryptionKeyFragment); + return Err(errors::IdUriError::InvalidEncryptionKeyFragment); } - } else { - Self::new(network, subnet.as_deref(), role0_pk, role_index, rotation) } + + if let Some(username) = username { + cat_id = cat_id.with_username(&username); + } + + if let Some(nonce) = nonce { + cat_id = cat_id.with_specific_nonce(nonce); + } + + // Default to URI, so only set it as an ID if its not a URI. + if id { + cat_id = cat_id.as_id(); + } + + cat_id }; - Ok(kid) + Ok(cat_id) } } -impl Display for KidUri { +impl Display for IdUri { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}://", Self::SCHEME.as_str())?; + if !self.id { + write!(f, "{}://", Self::SCHEME.as_str())?; + } + + let mut needs_at = false; + if let Some(username) = &self.username { + write!(f, "{username}")?; + needs_at = true; + } + + if let Some(nonce) = self.nonce { + let timestamp = nonce.timestamp(); + write!(f, ":{timestamp}")?; + needs_at = true; + } + + // If we had a username OR a nonce, then we need an `@` to separate from the hostname. + if needs_at { + write!(f, "@")?; + } + if let Some(subnet) = &self.subnet { - write!(f, "{subnet}@")?; + write!(f, "{subnet}.")?; } write!( f, - "{}/{}/{}/{}", + "{}/{}", self.network, base64_url::encode(self.role0_pk.as_bytes()), - self.role, - self.rotation )?; + + // Role and Rotation are only serialized if its NOT and ID or they are not the defaults. + if !self.role.is_default() || !self.rotation.is_default() || !self.id { + write!(f, "/{}", self.role)?; + if !self.rotation.is_default() || !self.id { + write!(f, "/{}", self.rotation)?; + } + } + if self.encryption { write!(f, "#{}", Self::ENCRYPTION_FRAGMENT)?; } @@ -190,12 +472,12 @@ impl Display for KidUri { } } -impl TryFrom<&[u8]> for KidUri { - type Error = errors::KidUriError; +impl TryFrom<&[u8]> for IdUri { + type Error = errors::IdUriError; fn try_from(value: &[u8]) -> Result { let kid_str = String::from_utf8_lossy(value); - KidUri::from_str(&kid_str) + IdUri::from_str(&kid_str) } } @@ -204,24 +486,41 @@ mod tests { use ed25519_dalek::SigningKey; use rand::rngs::OsRng; - use super::KidUri; - - const KID_TEST_VECTOR: [&str; 5] = [ - "kid.catalyst-rbac://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/0", - "kid.catalyst-rbac://preprod@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3", - "kid.catalyst-rbac://preprod@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/0#encrypt", - "kid.catalyst-rbac://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/1", - "kid.catalyst-rbac://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/1#encrypt" + use super::IdUri; + + const ID_URI_TEST_VECTOR: [&str; 9] = [ + "cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE", + "user@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE", + "user:1735689600@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE", + ":1735689600@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE", + "cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE", + "id.catalyst://preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3", + "id.catalyst://preview.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/0#encrypt", + "id.catalyst://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/1", + "id.catalyst://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/1#encrypt", ]; #[test] - fn test_kid_uri_from_str() { - for kid_string in KID_TEST_VECTOR { - let kid = kid_string.parse::().unwrap(); - assert_eq!(format!("{kid}"), kid_string); + /// Tests that deserialization and re-serialization round trip correctly + fn test_id_uri_from_str() { + for id_string in ID_URI_TEST_VECTOR { + let id = id_string.parse::().unwrap(); + assert_eq!(format!("{id}"), id_string); } } + #[test] + /// Tests that a short form of a long ID is the same as a short deserialized ID + fn test_short_id() { + let test_uri = "id.catalyst://user:1735689600@preview.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/0#encrypt"; + let expected_id = "preview.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE"; + + let uri_id = test_uri.parse::().unwrap(); + let short_id = expected_id.parse::().unwrap(); + + assert_eq!(uri_id.as_short_id(), short_id); + } + #[ignore] #[test] fn gen_pk() { diff --git a/rust/catalyst-types/src/id_uri/role_index.rs b/rust/catalyst-types/src/id_uri/role_index.rs index 24b795273e..d45071c4f2 100644 --- a/rust/catalyst-types/src/id_uri/role_index.rs +++ b/rust/catalyst-types/src/id_uri/role_index.rs @@ -23,6 +23,23 @@ pub enum RoleIndexError { #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct RoleIndex(u16); +impl RoleIndex { + /// Default Role Index + pub const DEFAULT: RoleIndex = RoleIndex(0); + + /// Is the `RoleIndex` the default value + #[must_use] + pub fn is_default(self) -> bool { + self == Self::DEFAULT + } +} + +impl Default for RoleIndex { + fn default() -> Self { + Self::DEFAULT + } +} + impl From for RoleIndex { fn from(value: u16) -> Self { Self(value) diff --git a/rust/catalyst-types/tests/mod.rs b/rust/catalyst-types/tests/mod.rs index e8d5156342..97112797ee 100644 --- a/rust/catalyst-types/tests/mod.rs +++ b/rust/catalyst-types/tests/mod.rs @@ -12,7 +12,7 @@ fn test_type_usage() { type D = catalyst_types::uuid::V4; type E = catalyst_types::uuid::V7; - type F = catalyst_types::id_uri::KidUri; + type F = catalyst_types::id_uri::IdUri; let bytes: [u8; 32] = [0; 32]; let _ = catalyst_types::hashes::Blake2bHash::from(bytes); From 5961bdd6ff064eeccc81300b772dec6ff400b10d Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Sun, 19 Jan 2025 15:11:42 +0700 Subject: [PATCH 05/18] docs(docs): cleanup the project dict --- .config/dictionaries/project.dic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 4e80f2a81b..da5bb811b0 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -238,8 +238,8 @@ slotno smac stevenj stringzilla -subsec subnetwork +subsec symlinkat syscall tacho From 22e0d29fcdf54b52bfc845514e2f28b19b7de987 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Mon, 20 Jan 2025 09:16:31 +0700 Subject: [PATCH 06/18] docs(docs): Replace with a concrete valid example public key --- .../rbac_id_uri/catalyst-id-uri.md | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md b/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md index 64ade93b92..71a8db9203 100644 --- a/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md +++ b/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md @@ -132,6 +132,7 @@ The overall `path` specification is: `//#encr * This does not change, even if the Initial Role 0 key is revoked. * This allows for an unambiguous identifier for the RBAC keychain. * It is not necessarily the key being identified. + * An example Role 0 Key is `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE` * `` - *Optional* This is the Role number being used. * It is a positive number, starting at 0, and no greater than 65535. * If it is not defined, then its default value is 0. @@ -154,44 +155,48 @@ The first implementation will be Catalyst Voices. ## Test Vectors -* `id.catalyst://cardano/` +* `id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE` * A Signing key registered on the Cardano Main network. * Role 0 - Rotation 0. * `username` - undefined. * `nonce` - undefined. - * In this example, it is identical to `/0/0` or `/0`. -* `id.catalyst://cardano//0` + * In this example, it is identical to `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/0` or + `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0`. +* `id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0` * A Signing key registered on the Cardano Main network. * Role 0 - Rotation 0. * `username` - undefined. * `nonce` - undefined. - * In this example, it is identical to `/0/0` or ``. -* `id.catalyst://gary@cardano//0/0` + * In this example, it is identical to `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/0` or + `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE`. +* `id.catalyst://gary@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/0` * A Signing key registered on the Cardano Main network. * Role 0 - Rotation 0. * `username` - `gary`. * `nonce` - undefined. - * In this example, it is identical to `` or `/0`. -* `id.catalyst://faith@preprod@cardano//7/3` + * In this example, it is identical to `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE` or + `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0`. +* `id.catalyst://faith@preprod@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3` * A Signing key registered on the Cardano pre-production network. * Role 7 - Rotation 3. * `username` - `faith` * `nonce` - undefined. * The Key for Role 7, and its third published rotation (i.e., the fourth key published, the first is the initial key, plus 3 rotations following it). -* `id.catalyst://faith:173710179@preprod@cardano//2/0#encrypt` +* `id.catalyst://faith:173710179@preprod@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/0#encrypt` * A Public Encryption key registered on the Cardano pre-production network. * Role 2 - Rotation 0. * `username` - `faith` * `nonce` - 173710179. * The initially published Public Encryption Key for Role 2. -* `kid.catalyst-rbac://:173710179@midnight//0/1` +* `kid.catalyst-rbac://:173710179@midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/1` * A Signing key registered on the Midnight Blockchain Main network * Role 0 - Rotation 1. * `username` - undefined. * `nonce` - 173710179. - * In this example, it is NOT the same as the ``, as it identifies the first rotation after ``. -* `kid.catalyst-rbac://midnight//2/1#encrypt` + * In this example, it is NOT the same as the `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE`, + as it identifies the first rotation after `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE`. +* `kid.catalyst-rbac://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/1#encrypt` * A public encryption key registered on the Midnight Blockchain Main network. * Role 2 - Rotation 1. * `username` - undefined. From 1299f9748d7b52eaa10244eeca467475d5a6e74c Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Mon, 20 Jan 2025 09:23:55 +0700 Subject: [PATCH 07/18] docs(docs): Fix documentation inconsistencies --- .../08_concepts/rbac_id_uri/catalyst-id-uri.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md b/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md index 71a8db9203..69a41dbbf9 100644 --- a/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md +++ b/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md @@ -80,7 +80,7 @@ The [host](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2) refers to the network type where the RBAC registration was made. It **IS NOT** resolvable with **DNS**, and **IS NOT** a public host name. It is used as a decentralized network identifier. -The consumer of the `KID` must be able to resolve these host names. +The consumer of the `ID` must be able to resolve these host names to known and supported blockchain networks. The hostname may have one or more subdomains which could specify side-chains of a particular network, or test networks. @@ -118,7 +118,7 @@ Applications which use the `nonce` will define its use, anything that does not u * `anne@cardano` - username `anne` no nonce. * `blake:1737101079@midnight` - username `blake` with nonce 1737101079. -* `:173710179#ethereum` - no username with nonce 173710179. +* `:173710179@ethereum` - no username with nonce 173710179. ### `path` @@ -176,27 +176,27 @@ The first implementation will be Catalyst Voices. * `nonce` - undefined. * In this example, it is identical to `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE` or `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0`. -* `id.catalyst://faith@preprod@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3` +* `id.catalyst://faith@preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/3` * A Signing key registered on the Cardano pre-production network. * Role 7 - Rotation 3. * `username` - `faith` * `nonce` - undefined. * The Key for Role 7, and its third published rotation (i.e., the fourth key published, the first is the initial key, plus 3 rotations following it). -* `id.catalyst://faith:173710179@preprod@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/0#encrypt` +* `id.catalyst://faith:173710179@preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/0#encrypt` * A Public Encryption key registered on the Cardano pre-production network. * Role 2 - Rotation 0. * `username` - `faith` * `nonce` - 173710179. * The initially published Public Encryption Key for Role 2. -* `kid.catalyst-rbac://:173710179@midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/1` +* `id.catalyst://:173710179@midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/0/1` * A Signing key registered on the Midnight Blockchain Main network * Role 0 - Rotation 1. * `username` - undefined. * `nonce` - 173710179. * In this example, it is NOT the same as the `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE`, as it identifies the first rotation after `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE`. -* `kid.catalyst-rbac://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/1#encrypt` +* `id.catalyst://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/1#encrypt` * A public encryption key registered on the Midnight Blockchain Main network. * Role 2 - Rotation 1. * `username` - undefined. From 784ae1bfef7a2e14295c0d965c8101ab874d2d9a Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Mon, 20 Jan 2025 10:56:49 +0700 Subject: [PATCH 08/18] Update docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md Co-authored-by: bkioshn <35752733+bkioshn@users.noreply.github.com> --- .../src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md b/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md index 69a41dbbf9..1e9860a1bc 100644 --- a/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md +++ b/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md @@ -200,7 +200,7 @@ The first implementation will be Catalyst Voices. * A public encryption key registered on the Midnight Blockchain Main network. * Role 2 - Rotation 1. * `username` - undefined. - * `nonce` - 173710179. + * `nonce` - undefined. ## Rationale: how does this CIP achieve its goals? From 59a2ce42f94d6b63c20aeaa0ba418cb5bbe5d823 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Mon, 20 Jan 2025 10:59:47 +0700 Subject: [PATCH 09/18] fix(docs): Spelling issue --- .../src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md b/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md index 69a41dbbf9..7dc481644e 100644 --- a/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md +++ b/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md @@ -11,6 +11,8 @@ Created: 2025-01-05 License: CC-BY-4.0 --- + + * [Abstract](#abstract) * [Motivation: why is this CIP necessary?](#motivation-why-is-this-cip-necessary) * [Specification](#specification) From 5cda5d3a39aa5a5e6051f54bdfcd106090af31a8 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Mon, 20 Jan 2025 11:43:04 +0700 Subject: [PATCH 10/18] Update rust/catalyst-types/src/id_uri/mod.rs Co-authored-by: bkioshn <35752733+bkioshn@users.noreply.github.com> --- rust/catalyst-types/src/id_uri/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/catalyst-types/src/id_uri/mod.rs b/rust/catalyst-types/src/id_uri/mod.rs index 14f9f8235b..f4fde9f417 100644 --- a/rust/catalyst-types/src/id_uri/mod.rs +++ b/rust/catalyst-types/src/id_uri/mod.rs @@ -1,4 +1,4 @@ -//! COSE Signature Protected Header `kid`. +//! Catalyst ID URI. // cspell: words userinfo rngs Fftx csprng From 9ea0364d7fc18df7e5bc942d36043f6c37d71644 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Mon, 20 Jan 2025 11:43:14 +0700 Subject: [PATCH 11/18] Update docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md Co-authored-by: bkioshn <35752733+bkioshn@users.noreply.github.com> --- .../src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md b/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md index a20ba59672..1c0bcc21a6 100644 --- a/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md +++ b/docs/src/architecture/08_concepts/rbac_id_uri/catalyst-id-uri.md @@ -199,7 +199,7 @@ The first implementation will be Catalyst Voices. * In this example, it is NOT the same as the `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE`, as it identifies the first rotation after `FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE`. * `id.catalyst://midnight/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/2/1#encrypt` - * A public encryption key registered on the Midnight Blockchain Main network. + * A Public Encryption key registered on the Midnight Blockchain Main network. * Role 2 - Rotation 1. * `username` - undefined. * `nonce` - undefined. From f1303dcc5ff42123ccdf2ffae0b75d1fe10ddc10 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Mon, 20 Jan 2025 16:27:33 +0700 Subject: [PATCH 12/18] fix(rust): Remove role and rotation from new as we should use `with_role` and `with_rotation` to set them. --- rust/catalyst-types/src/id_uri/mod.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/rust/catalyst-types/src/id_uri/mod.rs b/rust/catalyst-types/src/id_uri/mod.rs index 14f9f8235b..d2e62c3194 100644 --- a/rust/catalyst-types/src/id_uri/mod.rs +++ b/rust/catalyst-types/src/id_uri/mod.rs @@ -112,20 +112,17 @@ impl IdUri { /// Create a new `IdUri` for a Signing Key #[must_use] - pub fn new( - network: &str, subnet: Option<&str>, role0_pk: VerifyingKey, role: RoleIndex, - rotation: KeyRotation, - ) -> Self { + pub fn new(network: &str, subnet: Option<&str>, role0_pk: VerifyingKey) -> Self { Self { username: None, // Default to Not set, use `with_username` if required. nonce: None, // Default to Not set, use `with_nonce` if required. network: network.to_string(), subnet: subnet.map(str::to_string), role0_pk, - role, - rotation, - encryption: false, - id: false, // Default to `URI` formatted. + role: RoleIndex::default(), // Defaulted, use `with_role()` to change it. + rotation: KeyRotation::default(), // Defaulted, use `with_rotation()` to change it. + encryption: false, // Defaulted, use `with_encryption()` to change it. + id: false, // Default to `URI` formatted. } } @@ -395,7 +392,10 @@ impl FromStr for IdUri { }; let cat_id = { - let mut cat_id = Self::new(network, subnet, role0_pk, role_index, rotation); + let mut cat_id = Self::new(network, subnet, role0_pk) + .with_role(role_index) + .with_rotation(rotation); + if uri.has_fragment() { if uri.fragment() == Some(Self::ENCRYPTION_FRAGMENT) { cat_id = cat_id.with_encryption(); From a4813943fba5558321edf5567fe8a7edb5f2385a Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Mon, 20 Jan 2025 16:36:11 +0700 Subject: [PATCH 13/18] fix(rust): Fail correctly if converting from &[u8] and the data isn't valid UTF8. --- rust/catalyst-types/src/id_uri/errors.rs | 2 ++ rust/catalyst-types/src/id_uri/mod.rs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/rust/catalyst-types/src/id_uri/errors.rs b/rust/catalyst-types/src/id_uri/errors.rs index eb8e31732f..80d8a00338 100644 --- a/rust/catalyst-types/src/id_uri/errors.rs +++ b/rust/catalyst-types/src/id_uri/errors.rs @@ -32,4 +32,6 @@ pub enum IdUriError { InvalidRotationValue(#[from] KeyRotationError), /// Encryption key Identifier Fragment is not valid InvalidEncryptionKeyFragment, + /// Invalid Text encoding + InvalidTextEncoding(#[from] std::string::FromUtf8Error), } diff --git a/rust/catalyst-types/src/id_uri/mod.rs b/rust/catalyst-types/src/id_uri/mod.rs index c5a1a40307..afacd966d6 100644 --- a/rust/catalyst-types/src/id_uri/mod.rs +++ b/rust/catalyst-types/src/id_uri/mod.rs @@ -395,7 +395,7 @@ impl FromStr for IdUri { let mut cat_id = Self::new(network, subnet, role0_pk) .with_role(role_index) .with_rotation(rotation); - + if uri.has_fragment() { if uri.fragment() == Some(Self::ENCRYPTION_FRAGMENT) { cat_id = cat_id.with_encryption(); @@ -476,7 +476,7 @@ impl TryFrom<&[u8]> for IdUri { type Error = errors::IdUriError; fn try_from(value: &[u8]) -> Result { - let kid_str = String::from_utf8_lossy(value); + let kid_str = String::from_utf8(value.to_vec())?; IdUri::from_str(&kid_str) } } From 4435c5cf9400212b63db04ccba5d8d3f06c35930 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Tue, 21 Jan 2025 22:54:59 +0700 Subject: [PATCH 14/18] docs(rust): fix comment --- rust/catalyst-types/src/id_uri/key_rotation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/catalyst-types/src/id_uri/key_rotation.rs b/rust/catalyst-types/src/id_uri/key_rotation.rs index 02ea2ae404..44887242dc 100644 --- a/rust/catalyst-types/src/id_uri/key_rotation.rs +++ b/rust/catalyst-types/src/id_uri/key_rotation.rs @@ -22,7 +22,7 @@ pub enum KeyRotationError { pub struct KeyRotation(u16); impl KeyRotation { - /// Default Role Index + /// Default Key Rotation pub const DEFAULT: KeyRotation = KeyRotation(0); /// Is the `KeyRotation` the default value From 70cb89d0efafab821cba02b539020fd81c2a9422 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Tue, 21 Jan 2025 22:55:27 +0700 Subject: [PATCH 15/18] docs(docs): add nsecs to project dictionary --- .config/dictionaries/project.dic | 1 + 1 file changed, 1 insertion(+) diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index da5bb811b0..cdd52f173a 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -168,6 +168,7 @@ nextest nolfs notadb nsec +nsecs OCSP Oleksandr oneshot From fc395587d7f449f3b46fc60fc118e8e528eef985 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Tue, 21 Jan 2025 22:56:02 +0700 Subject: [PATCH 16/18] fix(rust): clamp a specific nonce, in case it was attempted to be set out of range. --- rust/catalyst-types/src/id_uri/mod.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/rust/catalyst-types/src/id_uri/mod.rs b/rust/catalyst-types/src/id_uri/mod.rs index afacd966d6..724ae3a58e 100644 --- a/rust/catalyst-types/src/id_uri/mod.rs +++ b/rust/catalyst-types/src/id_uri/mod.rs @@ -163,12 +163,23 @@ impl IdUri { } /// Add or change the nonce to a specific value in a Catalyst ID URI. + /// + /// Note, this will not fail, but if the Datetime is < `MIN_NONCE`, + /// or greater than `MAX_NONCE`, it will be clamped into that range. #[must_use] pub fn with_specific_nonce(self, nonce: DateTime) -> Self { - Self { - nonce: Some(nonce), - ..self - } + let secs = nonce.timestamp(); + let clamped_secs = secs.clamp(Self::MIN_NONCE, Self::MAX_NONCE); + + let nonce = { + if clamped_secs == secs { + Some(nonce) + } else { + DateTime::::from_timestamp(clamped_secs, 0) + } + }; + + Self { nonce, ..self } } /// Add or change the nonce in a Catalyst ID URI. From 1ab9e14b8c8347d8d215b60c09af51a724b73d38 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Thu, 23 Jan 2025 14:50:49 +0700 Subject: [PATCH 17/18] docs(rust): improve documentation of a number of public functions in the `IdUri` type. --- rust/catalyst-types/src/id_uri/mod.rs | 196 ++++++++++++++++++++++++-- 1 file changed, 181 insertions(+), 15 deletions(-) diff --git a/rust/catalyst-types/src/id_uri/mod.rs b/rust/catalyst-types/src/id_uri/mod.rs index 724ae3a58e..9d49d52d91 100644 --- a/rust/catalyst-types/src/id_uri/mod.rs +++ b/rust/catalyst-types/src/id_uri/mod.rs @@ -2,9 +2,9 @@ // cspell: words userinfo rngs Fftx csprng -mod errors; -mod key_rotation; -mod role_index; +pub mod errors; +pub mod key_rotation; +pub mod role_index; use std::{ fmt::{Display, Formatter}, @@ -162,10 +162,27 @@ impl IdUri { } } - /// Add or change the nonce to a specific value in a Catalyst ID URI. + /// Add or change the nonce (a unique identifier for a data update) to a specific + /// value in a Catalyst `IdUri`. /// - /// Note, this will not fail, but if the Datetime is < `MIN_NONCE`, - /// or greater than `MAX_NONCE`, it will be clamped into that range. + /// This method is intended for use with trusted data where the nonce is known and + /// verified beforehand. It ensures that the provided nonce is within the valid + /// range, clamping it if necessary between `MIN_NONCE` and `MAX_NONCE`. + /// Properly generated or trusted nonces will not be altered by this function. + /// + /// # Parameters + /// - `nonce`: A `DateTime` representing the specific nonce value to set in the + /// Catalyst `IdUri`. This should be a valid UTC datetime. + /// + /// # Returns + /// The updated Catalyst `IdUri` with the specified nonce, if it was within the + /// allowed range; otherwise, it will be updated with a clamped value of the + /// nonce. + /// + /// # Safety + /// - **Pre-validation of the nonce is required**: If you are working with untrusted + /// data, ensure that the nonce has been pre-validated and take appropriate action + /// before calling this function. #[must_use] pub fn with_specific_nonce(self, nonce: DateTime) -> Self { let secs = nonce.timestamp(); @@ -182,13 +199,44 @@ impl IdUri { Self { nonce, ..self } } - /// Add or change the nonce in a Catalyst ID URI. + /// Add or change the nonce in a Catalyst ID URI. The nonce will be set to the current + /// UTC time when this method is called. + /// + /// This function returns a new instance of the type with the nonce field updated to + /// the current UTC time. + /// + /// # Examples + /// ```rust + /// use catalyst_types::id_uri::IdUri; + /// use chrono::Utc; + /// + /// let id_uri = IdUri::default(); + /// let id_uri_with_nonce = id_uri.with_nonce(); + /// assert!(id_uri_with_nonce.nonce.is_some()); + /// ``` #[must_use] pub fn with_nonce(self) -> Self { self.with_specific_nonce(Utc::now()) } /// Set that there is no Nonce in the ID or URI + /// Represents an ID or URI without a Nonce. + /// + /// This method creates a new instance of the type, but sets the nonce field to + /// `None`. The rest of the fields are inherited from the original instance. + /// + /// # Examples + /// ```rust + /// use catalyst_types::id_uri::IdUri; + /// use chrono::{DateTime, Duration, Utc}; + /// let id_uri = "id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE" + /// .parse::() + /// .unwrap() + /// .with_nonce(); + /// + /// let id_uri_without_nonce = id_uri.without_nonce(); + /// assert_eq!(id_uri_without_nonce.nonce(), None); + /// ``` #[must_use] pub fn without_nonce(self) -> Self { Self { @@ -197,7 +245,27 @@ impl IdUri { } } - /// Create a new `IdUri` for an Encryption Key + /// Set that the `IdUri` is used to identify an encryption key. + /// + /// This method sets `IdUri` is identifying an encryption key. + /// + /// # Returns + /// + /// A new instance of the type with the updated encryption flag. + /// + /// # Examples + /// + /// ```rust + /// use catalyst_types::id_uri::IdUri; + /// + /// let id_uri = "id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE" + /// .parse::() + /// .unwrap(); + /// assert_eq!(id_uri.is_encryption_key(), false); + /// + /// let id_uri = id_uri.with_encryption(); + /// assert_eq!(id_uri.is_encryption_key(), true); + /// ``` #[must_use] pub fn with_encryption(self) -> Self { Self { @@ -206,7 +274,27 @@ impl IdUri { } } - /// Set that the ID is not for encryption + /// Set that the `IdUri` is not for encryption + /// + /// This method sets `IdUri` is not identifying an encryption key. + /// + /// # Returns + /// + /// A new instance of the type with the updated encryption flag. + /// + /// # Examples + /// + /// ```rust + /// use catalyst_types::id_uri::IdUri; + /// + /// let id_uri = "id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE#encrypt" + /// .parse::() + /// .unwrap(); + /// assert_eq!(id_uri.is_encryption_key(), true); + /// + /// let id_uri = id_uri.without_encryption(); + /// assert_eq!(id_uri.is_encryption_key(), false); + /// ``` #[must_use] pub fn without_encryption(self) -> Self { Self { @@ -215,13 +303,60 @@ impl IdUri { } } - /// Set the role explicitly + /// Set the role explicitly. + /// + /// This method sets the role field to the specified value while leaving other + /// fields unchanged. + /// + /// # Parameters + /// - `role`: The new value for the role field. + /// + /// # Returns + /// + /// A new instance of the type with the updated role field. + /// + /// # Examples + /// + /// ```rust + /// use catalyst_types::id_uri::{role_index::RoleIndex, IdUri}; + /// + /// let id_uri = "id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE" + /// .parse::() + /// .unwrap(); + /// let new_role: RoleIndex = 5.into(); + /// let id_uri_with_role = id_uri.with_role(new_role); + /// let (role, _) = id_uri_with_role.role_and_rotation(); + /// assert_eq!(role, new_role); + /// ``` #[must_use] pub fn with_role(self, role: RoleIndex) -> Self { Self { role, ..self } } - /// Set the rotation explicitly + /// Set the rotation explicitly. + /// + /// This method sets the rotation field to the specified value while leaving other + /// fields unchanged. + /// + /// # Parameters + /// - `rotation`: The new value for the rotation field. 0 = First Key, 1+ is each + /// subsequent rotation. + /// + /// # Returns + /// A new instance of the type with the updated rotation field. + /// + /// # Examples + /// ```rust + /// use catalyst_types::id_uri::{key_rotation::KeyRotation, IdUri}; + /// + /// let id_uri = "id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE" + /// .parse::() + /// .unwrap(); + /// let new_rotation: KeyRotation = 4.into(); + /// let id_uri_with_rotation = id_uri.with_rotation(new_rotation); + /// let (_, rotation) = id_uri_with_rotation.role_and_rotation(); + /// assert_eq!(rotation, new_rotation); + /// ``` #[must_use] pub fn with_rotation(self, rotation: KeyRotation) -> Self { Self { rotation, ..self } @@ -286,10 +421,41 @@ impl IdUri { } } - /// Convert the `IdUri` to its shortest form. - /// This is an ID without any role/rotation information, no scheme, no username or - /// nonce. - /// This is used to get the most generalized form of a Catalyst ID. + /// Converts the `IdUri` to its shortest form. + /// This method returns a new instance of the type with no role information, no + /// scheme, no username, no nonce, and no encryption settings. It effectively + /// strips away all additional metadata to provide a most generalized form of the + /// Catalyst ID. + /// + /// # Returns + /// + /// A new `IdUri` instance representing the shortest form of the current `IdUri`. + /// + /// # Examples + /// + /// ```rust + /// use catalyst_types::id_uri::{key_rotation::KeyRotation, role_index::RoleIndex, IdUri}; + /// + /// let id_uri = + /// "id.catalyst://user:1735689600@cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE/7/5" + /// .parse::() + /// .unwrap(); + /// + /// let short_id = id_uri.as_short_id(); + /// assert_eq!( + /// short_id.role_and_rotation(), + /// (RoleIndex::default(), KeyRotation::default()) + /// ); + /// assert_eq!(short_id.username(), None); + /// assert_eq!(short_id.nonce(), None); + /// assert_eq!(short_id.is_encryption_key(), false); + /// + /// let short_id_str = format!("{short_id}"); + /// assert_eq!( + /// short_id_str, + /// "cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE" + /// ) + /// ``` #[must_use] pub fn as_short_id(&self) -> Self { self.clone() From 25625192b2eef9af90e85f51c3b79c7881331df2 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Fri, 24 Jan 2025 20:04:10 +0700 Subject: [PATCH 18/18] fix(rust): Fix doc test --- rust/catalyst-types/src/id_uri/mod.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rust/catalyst-types/src/id_uri/mod.rs b/rust/catalyst-types/src/id_uri/mod.rs index 9d49d52d91..af664f80b5 100644 --- a/rust/catalyst-types/src/id_uri/mod.rs +++ b/rust/catalyst-types/src/id_uri/mod.rs @@ -210,9 +210,12 @@ impl IdUri { /// use catalyst_types::id_uri::IdUri; /// use chrono::Utc; /// - /// let id_uri = IdUri::default(); + /// let id_uri = "id.catalyst://cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE" + /// .parse::() + /// .unwrap(); + /// assert!(id_uri.nonce().is_none()); /// let id_uri_with_nonce = id_uri.with_nonce(); - /// assert!(id_uri_with_nonce.nonce.is_some()); + /// assert!(id_uri_with_nonce.nonce().is_some()); /// ``` #[must_use] pub fn with_nonce(self) -> Self {