gabriel / muse public
hdkeys.py python
815 lines 27.4 KB
Raw
sha256:2a703f78341332ef0beb9856d2267de6aec89b3883c31519b6900b667d026e62 chore: delete muse/prose domain — hallucinated, never existed Sonnet 4.6 minor ⚠ breaking 5 days ago
1 """muse.core.hdkeys — Muse HD key derivation: six-level domain-first path.
2
3 Muse derives all cryptographic keys from a single BIP39 mnemonic via SLIP-0010
4 Ed25519 hierarchical deterministic derivation. The path structure is designed
5 to be a **semantic coordinate system** — every level answers exactly one
6 question, and every key's purpose is readable from its path alone.
7
8 Path structure
9 --------------
10 ::
11
12 m / purpose' / domain' / entity_type' / entity_id' / role' / index'
13 │ │ │ │ │ │
14 │ │ │ │ │ └── Which rotation? 0'=current, 1'=pre-rotated, …
15 │ │ │ │ └─────────── What does it do? 0'=sign, 1'=receive, 2'=provision, 3'=attest, 4'=delegate
16 │ │ │ └───────────────────────── Which specific? 0', 1', 2', …
17 │ │ └───────────────────────────────────────── What class? 0'=human, 1'=agent, 2'=org
18 │ └──────────────────────────────────────────────────── What universe? 0'=identity, 1'=payments, 2'=code, 3'=music, 4'=midi, 5'=blockchain, …
19 └──────────────────────────────────────────────────────────────────── What app? 1_075_233_755' (sha256(b"muse")[:4] & 0x7FFFFFFF)
20
21 Purpose
22 -------
23 ``1_075_233_755 = int.from_bytes(sha256(b"muse")[:4], "big") & 0x7FFFFFFF``
24
25 Reproducible by anyone — not an arbitrary number. The high bit is masked to
26 keep it in the valid unhardened range before the hardened offset is applied.
27
28 Domains
29 -------
30 Domains are **first-class entities** in the Muse key namespace. A key
31 belongs to a domain before it belongs to an entity — the domain scopes the
32 entire sub-tree beneath it.
33
34 Domain indices are **hash-derived** — ``sha256(name)[:4] & 0x7FFFFFFF`` —
35 using the same pattern as ``MUSE_PURPOSE``. This makes the namespace open:
36 any third party can register a domain without a central committee.
37
38 .. list-table::
39 :widths: 25 65
40 :header-rows: 1
41
42 * - Constant
43 - Meaning
44 * - :data:`DOMAIN_IDENTITY` (``domain_index("muse/identity")``)
45 - Cross-domain auth. The key that answers "who are you?" on the Muse
46 network. Used for MSign HTTP signing and MuseHub registration.
47 One per human or agent — it is their passport, not their work credential.
48 * - :data:`DOMAIN_PAYMENTS` (``domain_index("muse/payments")``)
49 - MPay claims, financial settlement.
50 * - :data:`DOMAIN_CODE` (``domain_index("muse/code")``)
51 - Software VCS — commit provenance, code-review attestations.
52 * - :data:`DOMAIN_MUSIC` (``domain_index("muse/music")``)
53 - Stori audio production — project signing, master ownership.
54 * - :data:`DOMAIN_MIDI` (``domain_index("muse/midi")``)
55 - Maestro symbolic music — NL→MIDI content signing.
56 * - :data:`DOMAIN_BLOCKCHAIN` (``domain_index("muse/blockchain")``)
57 - On-chain operations (ERC-8004 identity, ERC-721, AVAX).
58 secp256k1 keys for this domain use the ``b"Bitcoin seed"``
59 SLIP-0010 HMAC root — same path grammar, different curve.
60 * - :data:`DOMAIN_GENERIC` (``domain_index("muse/generic")``)
61 - Repos and entities with no registered domain plugin.
62 First-class explicit value — never an empty string or None.
63
64 Entity types
65 ------------
66 .. list-table::
67 :widths: 10 25 65
68 :header-rows: 1
69
70 * - Value
71 - Constant
72 - Meaning
73 * - 0
74 - :data:`ENTITY_HUMAN`
75 - Human operator. Account 0 is always the primary identity.
76 * - 1
77 - :data:`ENTITY_AGENT`
78 - AI agent. Each agent slot receives a domain-scoped sub-seed
79 from the operator — it cannot derive keys outside its granted domains.
80 * - 2
81 - :data:`ENTITY_ORG`
82 - Organisation or DAO. Governance and membership live above the key layer;
83 the key tree records only that this principal is a collective.
84
85 Roles
86 -----
87 .. list-table::
88 :widths: 10 25 65
89 :header-rows: 1
90
91 * - Value
92 - Constant
93 - Meaning
94 * - 0
95 - :data:`ROLE_SIGN`
96 - Primary signing key for this domain (default).
97 * - 1
98 - :data:`ROLE_RECEIVE`
99 - Receiving / payment address key.
100 * - 2
101 - :data:`ROLE_PROVISION`
102 - Provisioning key — used during entity bootstrapping.
103 * - 3
104 - :data:`ROLE_ATTEST`
105 - Third-party attestation key (distinct from self-signing).
106 * - 4
107 - :data:`ROLE_DELEGATE`
108 - Scoped authority delegation (future).
109
110 Domain-scoped agent delegation
111 -------------------------------
112 An agent's sub-seed is derived from the parent's key tree at the domain level.
113 This means an agent's capability is bounded by cryptography, not policy:
114
115 ::
116
117 # Operator grants a music agent only music-domain keys
118 music_agent_seed = derive_agent_sub_seed(master_seed, domain=DOMAIN_MUSIC, agent_id=0)
119
120 # A separate identity grant is needed for MuseHub auth
121 auth_agent_seed = derive_agent_sub_seed(master_seed, domain=DOMAIN_IDENTITY, agent_id=0)
122
123 # Agent uses each sub-seed independently — two separate key roots
124 agent_identity_key = derive_identity_key(auth_agent_seed)
125 agent_music_key = derive_domain_key(music_agent_seed, domain=DOMAIN_MUSIC)
126
127 Compromising a music agent's seed cannot reveal the operator's identity key
128 or any other domain's keys — SLIP-0010 hardened derivation guarantees this.
129
130 Sub-seed composition
131 --------------------
132 ``agent_sub_seed = dk.private_bytes + dk.chain_code`` (64 bytes)
133
134 Both halves are required: the chain code enables further child derivation from
135 the sub-seed root. The sub-seed is treated identically to a BIP39 master seed
136 by all functions in this module.
137
138 Two-tree architecture for blockchain
139 -------------------------------------
140 Ed25519 keys (domains 0–5) and secp256k1 keys (domain 6) are derived from
141 separate SLIP-0010 roots that share the same BIP39 mnemonic::
142
143 seed → HMAC("ed25519 seed", seed) → Ed25519 master (identity, payments, code, music, …)
144 seed → HMAC("Bitcoin seed", seed) → secp256k1 master (blockchain / EVM / AVAX)
145
146 Both trees use the same six-level path grammar. Blockchain-specific callers
147 use the secp256k1 master key directly; this module handles only Ed25519.
148
149 Examples
150 --------
151 ::
152
153 from muse.core.bip39 import generate_mnemonic, mnemonic_to_seed
154 from muse.core.hdkeys import (
155 derive_identity_key, derive_domain_key, derive_agent_sub_seed,
156 dk_to_ed25519, public_bytes_from_seed,
157 DOMAIN_IDENTITY, DOMAIN_MUSIC, ENTITY_HUMAN, ENTITY_AGENT, ROLE_SIGN,
158 )
159
160 mnemonic = generate_mnemonic()
161 seed = mnemonic_to_seed(mnemonic)
162
163 # Human operator's MuseHub identity (auth) key
164 dk = derive_identity_key(seed)
165 priv = dk_to_ed25519(dk)
166 pub_bytes = priv.public_key().public_bytes_raw() # 32 bytes → register with MuseHub
167
168 # Human operator's music signing key (Stori project provenance)
169 music_dk = derive_domain_key(seed, domain=DOMAIN_MUSIC)
170
171 # Spawn a music agent with a domain-scoped sub-seed
172 agent_seed = derive_agent_sub_seed(seed, domain=DOMAIN_MUSIC, agent_id=0)
173 agent_dk = derive_identity_key(agent_seed) # agent's own identity within music domain
174 """
175
176 import hashlib
177 from typing import TYPE_CHECKING
178
179 from muse.core.slip010 import (
180 MUSE_PURPOSE,
181 DerivedKey,
182 SecretByteArray,
183 Slip010Error,
184 child_key,
185 derive_path,
186 hardened,
187 master_key,
188 to_ed25519_private_key,
189 )
190
191 if TYPE_CHECKING:
192 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
193
194 __all__ = [
195 # Errors
196 "HdKeyError",
197 # Domain index function
198 "domain_index",
199 # Domain constants
200 "DOMAIN_IDENTITY",
201 "DOMAIN_PAYMENTS",
202 "DOMAIN_CODE",
203 "DOMAIN_MUSIC",
204 "DOMAIN_MIDI",
205 "DOMAIN_BLOCKCHAIN",
206 "DOMAIN_GENERIC",
207 # Entity type constants
208 "ENTITY_HUMAN",
209 "ENTITY_AGENT",
210 "ENTITY_ORG",
211 # Role constants
212 "ROLE_SIGN",
213 "ROLE_RECEIVE",
214 "ROLE_PROVISION",
215 "ROLE_ATTEST",
216 "ROLE_DELEGATE",
217 # Agent slot mapping
218 "agent_id_to_slot",
219 # Path helper
220 "muse_path",
221 # Core derivation
222 "derive_key",
223 "derive_identity_key",
224 "derive_domain_key",
225 "derive_agent_sub_seed",
226 # Key materialisation
227 "dk_to_ed25519",
228 "public_bytes_from_seed",
229 ]
230
231 # ---------------------------------------------------------------------------
232 # Domain index function
233 # ---------------------------------------------------------------------------
234
235 def domain_index(name: str) -> int:
236 """Return the canonical BIP32-compatible domain index for a named domain.
237
238 Uses the first 4 bytes of ``sha256(name.encode("utf-8"))`` interpreted as a
239 big-endian ``uint32`` masked to ``[0, 2^31 - 1]`` — the same pattern used
240 to derive ``MUSE_PURPOSE`` from ``b"muse"``.
241
242 This makes the domain namespace **open and decentralised**: any third-party
243 domain can compute its own index without a central registry. The birthday
244 bound for a 31-bit space is ~65k domains before a collision becomes likely —
245 far beyond any realistic deployment.
246
247 .. warning::
248 This mapping is **permanent**. Changing the algorithm or the canonical
249 name string for an existing domain invalidates every key ever derived
250 for that domain. Canonical name strings use the ``"muse/<slug>"``
251 convention for first-party domains.
252
253 Parameters
254 ----------
255 name:
256 Canonical domain name string (e.g. ``"muse/identity"``, ``"muse/code"``).
257
258 Returns
259 -------
260 int
261 Domain index in ``[0, 2^31 - 1]``.
262 """
263 digest = hashlib.sha256(name.encode("utf-8")).digest()
264 return int.from_bytes(digest[:4], "big") & 0x7FFF_FFFF
265
266 # ---------------------------------------------------------------------------
267 # Domain constants
268 # ---------------------------------------------------------------------------
269
270 #: Cross-domain authentication. The "passport" key — used for MSign HTTP
271 #: signing and MuseHub registration. One per human or agent.
272 DOMAIN_IDENTITY: int = domain_index("muse/identity")
273
274 #: MPay claims and financial settlement.
275 DOMAIN_PAYMENTS: int = domain_index("muse/payments")
276
277 #: Software VCS — commit provenance, code-review attestations.
278 DOMAIN_CODE: int = domain_index("muse/code")
279
280 #: Stori audio production — project signing, master ownership.
281 DOMAIN_MUSIC: int = domain_index("muse/music")
282
283 #: Maestro symbolic music — NL→MIDI content signing.
284 DOMAIN_MIDI: int = domain_index("muse/midi")
285
286 #: On-chain operations — ERC-8004, ERC-721, ERC-1155, AVAX.
287 #: secp256k1 keys for this domain use a separate SLIP-0010 root
288 #: (``b"Bitcoin seed"`` HMAC key) — same path grammar, different curve.
289 DOMAIN_BLOCKCHAIN: int = domain_index("muse/blockchain")
290
291 #: Repos and entities with no registered domain plugin.
292 #: First-class explicit value — never an empty string or None.
293 DOMAIN_GENERIC: int = domain_index("muse/generic")
294
295 # ---------------------------------------------------------------------------
296 # Entity type constants
297 # ---------------------------------------------------------------------------
298
299 #: Human operator.
300 ENTITY_HUMAN: int = 0
301
302 #: AI agent. Each agent slot receives a domain-scoped sub-seed.
303 ENTITY_AGENT: int = 1
304
305 #: Organisation or DAO.
306 ENTITY_ORG: int = 2
307
308 # ---------------------------------------------------------------------------
309 # Role constants
310 # ---------------------------------------------------------------------------
311
312 #: Primary signing key for a domain (default).
313 ROLE_SIGN: int = 0
314
315 #: Receiving / payment address key.
316 ROLE_RECEIVE: int = 1
317
318 #: Provisioning key — entity bootstrapping.
319 ROLE_PROVISION: int = 2
320
321 #: Third-party attestation key (distinct from self-signing).
322 ROLE_ATTEST: int = 3
323
324 #: Scoped authority delegation (reserved).
325 ROLE_DELEGATE: int = 4
326
327 # ---------------------------------------------------------------------------
328 # Errors
329 # ---------------------------------------------------------------------------
330
331 class HdKeyError(ValueError):
332 """Raised when an HD key derivation request is invalid.
333
334 Common causes:
335
336 - Negative domain, entity_id, or index value.
337 - Entity type or role outside the defined range.
338 - Agent sub-seed requested for the human entity (account 0 is the
339 human operator; sub-seeds are for agents only, ``entity_id >= 0``
340 under ``ENTITY_AGENT``).
341 - Seed shorter than 16 bytes (propagated from
342 :class:`~muse.core.slip010.Slip010Error`).
343
344 Subclasses :class:`ValueError` for broad compatibility. Use
345 ``except HdKeyError`` for precise handling.
346
347 Examples
348 --------
349 ::
350
351 try:
352 derive_key(seed, domain=-1)
353 except HdKeyError as exc:
354 print(f"key error: {exc}")
355 """
356
357 # ---------------------------------------------------------------------------
358 # Agent slot mapping
359 # ---------------------------------------------------------------------------
360
361 def agent_id_to_slot(agent_id: str) -> int:
362 """Map an agent handle string to a stable BIP32-compatible slot index.
363
364 Uses the first 4 bytes of ``sha256(agent_id.encode())`` interpreted as a
365 big-endian ``uint32`` masked to ``[0, 2^31 - 1]`` (the valid range before
366 the hardened offset is applied by SLIP-0010 callers).
367
368 Properties
369 ----------
370 - **Deterministic**: same handle always maps to the same slot.
371 - **Collision-resistant**: birthday bound at ~65k handles for a 2^32
372 pre-image space — sufficient for any realistic swarm size.
373 - **Platform-independent**: SHA-256 output is identical on all platforms.
374
375 .. warning::
376 This mapping is **permanent**. Changing the algorithm invalidates
377 every agent key ever derived. Never alter it after keys are in
378 production.
379
380 Parameters
381 ----------
382 agent_id:
383 Agent handle string (e.g. ``"claude-worker-01"``). The empty string
384 is accepted (maps to a deterministic slot) but not recommended.
385
386 Returns
387 -------
388 int
389 Slot index in ``[0, 2^31 - 1]``. Pass directly to
390 :func:`derive_agent_sub_seed` as the ``agent_id`` parameter.
391 """
392 digest = hashlib.sha256(agent_id.encode()).digest()
393 return int.from_bytes(digest[:4], "big") & 0x7FFF_FFFF
394
395 # ---------------------------------------------------------------------------
396 # Path helper
397 # ---------------------------------------------------------------------------
398
399 def muse_path(
400 domain: int,
401 entity_type: int = ENTITY_HUMAN,
402 entity_id: int = 0,
403 role: int = ROLE_SIGN,
404 index: int = 0,
405 ) -> str:
406 """Return the canonical Muse derivation path string for the given coordinates.
407
408 The returned string is suitable for passing directly to
409 :func:`~muse.core.slip010.derive_path`.
410
411 Parameters
412 ----------
413 domain:
414 Domain index (one of the ``DOMAIN_*`` constants). Must be >= 0.
415 entity_type:
416 Entity class (one of the ``ENTITY_*`` constants). Must be 0–3.
417 entity_id:
418 Entity slot within its type (0 = first, 1 = second, …). Must be >= 0.
419 role:
420 Key role within the domain (one of the ``ROLE_*`` constants). Must be 0–4.
421 index:
422 Key rotation index (0 = current, 1 = pre-rotated, …). Must be >= 0.
423
424 Returns
425 -------
426 str
427 Path such as ``"m/1075233755'/0'/0'/0'/0'/0'"``.
428
429 Raises
430 ------
431 HdKeyError
432 If any argument is out of range.
433
434 Examples
435 --------
436 ::
437
438 assert muse_path(DOMAIN_IDENTITY) == "m/1075233755'/1660078172'/0'/0'/0'/0'"
439 assert muse_path(DOMAIN_MUSIC, entity_type=ENTITY_AGENT, entity_id=1) \
440 == "m/1075233755'/1755707987'/1'/1'/0'/0'"
441 """
442 _validate_non_negative(domain, "domain")
443 _validate_entity_type(entity_type)
444 _validate_non_negative(entity_id, "entity_id")
445 _validate_role(role)
446 _validate_non_negative(index, "index")
447 return (
448 f"m/{MUSE_PURPOSE}'"
449 f"/{domain}'"
450 f"/{entity_type}'"
451 f"/{entity_id}'"
452 f"/{role}'"
453 f"/{index}'"
454 )
455
456 # ---------------------------------------------------------------------------
457 # Core derivation
458 # ---------------------------------------------------------------------------
459
460 def derive_key(
461 seed: bytes,
462 domain: int,
463 entity_type: int = ENTITY_HUMAN,
464 entity_id: int = 0,
465 role: int = ROLE_SIGN,
466 index: int = 0,
467 ) -> DerivedKey:
468 """Derive an Ed25519 key at the given Muse coordinates.
469
470 This is the general-purpose derivation primitive. For common cases prefer
471 :func:`derive_identity_key` or :func:`derive_domain_key` — they call this
472 internally with clearer call sites.
473
474 Path derived::
475
476 m / purpose' / domain' / entity_type' / entity_id' / role' / index'
477
478 Parameters
479 ----------
480 seed:
481 64-byte BIP39 seed from :func:`muse.core.bip39.mnemonic_to_seed`,
482 or a 64-byte agent sub-seed from :func:`derive_agent_sub_seed`.
483 domain:
484 Domain index (``DOMAIN_*`` constant).
485 entity_type:
486 Entity class (``ENTITY_*`` constant). Default: :data:`ENTITY_HUMAN`.
487 entity_id:
488 Entity slot within its type. Default: ``0`` (first).
489 role:
490 Key role within the domain (``ROLE_*`` constant). Default: :data:`ROLE_SIGN`.
491 index:
492 Key rotation index. 0 = current, 1 = pre-rotated. Default: ``0``.
493
494 Returns
495 -------
496 DerivedKey
497 SLIP-0010 private key and chain code at the requested coordinates.
498
499 Raises
500 ------
501 HdKeyError
502 If any argument is out of range.
503 Slip010Error
504 If *seed* is shorter than 16 bytes.
505
506 Examples
507 --------
508 ::
509
510 # Human operator's music signing key
511 dk = derive_key(seed, domain=DOMAIN_MUSIC)
512
513 # Agent slot 2's identity key
514 dk = derive_key(seed, domain=DOMAIN_IDENTITY, entity_type=ENTITY_AGENT, entity_id=2)
515 """
516 path = muse_path(domain, entity_type, entity_id, role, index)
517 return derive_path(seed, path)
518
519 def derive_identity_key(
520 seed: bytes,
521 entity_type: int = ENTITY_HUMAN,
522 entity_id: int = 0,
523 index: int = 0,
524 ) -> DerivedKey:
525 """Derive the cross-domain identity (MSign auth) key.
526
527 The identity key is the entity's **passport** on the Muse network. It is
528 used for:
529
530 - Ed25519 HTTP request signing (``X-Muse-Signature`` header)
531 - MuseHub authentication and handle registration
532 - Agent identity in agentception
533
534 This key belongs to :data:`DOMAIN_IDENTITY` and uses :data:`ROLE_SIGN`.
535 It is the same key regardless of which domain the entity is working in —
536 authentication is a cross-domain concern.
537
538 Parameters
539 ----------
540 seed:
541 64-byte BIP39 seed or agent sub-seed.
542 entity_type:
543 Entity class. Default: :data:`ENTITY_HUMAN`.
544 entity_id:
545 Entity slot. Default: ``0``.
546 index:
547 Rotation index. Default: ``0`` (current key).
548
549 Returns
550 -------
551 DerivedKey
552 Identity key at ``m/purpose'/0'/entity_type'/entity_id'/0'/index'``.
553
554 Examples
555 --------
556 ::
557
558 # Human operator's MuseHub auth key
559 dk = derive_identity_key(seed)
560 priv = dk_to_ed25519(dk)
561 pub = priv.public_key().public_bytes_raw() # register this with MuseHub
562
563 # Agent slot 0's identity key (auth_agent_seed from derive_agent_sub_seed)
564 agent_dk = derive_identity_key(auth_agent_seed, entity_type=ENTITY_AGENT)
565 """
566 return derive_key(
567 seed,
568 domain=DOMAIN_IDENTITY,
569 entity_type=entity_type,
570 entity_id=entity_id,
571 role=ROLE_SIGN,
572 index=index,
573 )
574
575 def derive_domain_key(
576 seed: bytes,
577 domain: int,
578 entity_type: int = ENTITY_HUMAN,
579 entity_id: int = 0,
580 role: int = ROLE_SIGN,
581 index: int = 0,
582 ) -> DerivedKey:
583 """Derive a domain-specific signing key.
584
585 Use this for keys that sign *content* within a particular domain — commit
586 provenance, music project signatures, payment claims, etc. The identity
587 key (:func:`derive_identity_key`) handles *authentication*; this function
588 handles *attestation*.
589
590 Parameters
591 ----------
592 seed:
593 64-byte BIP39 seed or agent sub-seed.
594 domain:
595 Domain index (``DOMAIN_CODE``, ``DOMAIN_MUSIC``, etc.).
596 :data:`DOMAIN_IDENTITY` is valid but :func:`derive_identity_key` is
597 preferred for that case.
598 entity_type:
599 Entity class. Default: :data:`ENTITY_HUMAN`.
600 entity_id:
601 Entity slot. Default: ``0``.
602 role:
603 Key role within the domain. Default: :data:`ROLE_SIGN`.
604 index:
605 Rotation index. Default: ``0``.
606
607 Returns
608 -------
609 DerivedKey
610 Domain key at the requested coordinates.
611
612 Examples
613 --------
614 ::
615
616 # Human operator's commit signing key
617 code_dk = derive_domain_key(seed, domain=DOMAIN_CODE)
618
619 # Human operator's Stori project signing key
620 music_dk = derive_domain_key(seed, domain=DOMAIN_MUSIC)
621
622 # Human operator's MPay payment key
623 pay_dk = derive_domain_key(seed, domain=DOMAIN_PAYMENTS, role=ROLE_RECEIVE)
624 """
625 return derive_key(seed, domain, entity_type, entity_id, role, index)
626
627 def derive_agent_sub_seed(
628 seed: bytes,
629 domain: int,
630 agent_id: int,
631 ) -> bytearray:
632 """Derive a 64-byte domain-scoped sub-seed for an agent.
633
634 Agent processes must never receive the operator's master seed. Instead,
635 the operator derives a **per-domain, per-agent sub-seed** and injects it
636 into the agent process. The agent treats this sub-seed exactly like a
637 regular BIP39 seed — it calls :func:`derive_identity_key` and
638 :func:`derive_domain_key` with it as normal.
639
640 The sub-seed is rooted at::
641
642 m / purpose' / domain' / ENTITY_AGENT' / agent_id'
643
644 So the agent's key tree sits entirely within the operator's ``domain``
645 sub-tree. The agent physically cannot derive keys in any other domain,
646 nor can it derive the operator's keys — SLIP-0010 hardened derivation
647 guarantees this.
648
649 Sub-seed composition::
650
651 sub_seed = dk.private_bytes + dk.chain_code # 64 bytes
652
653 Both halves are required: the chain code enables further child derivation.
654
655 Parameters
656 ----------
657 seed:
658 Master 64-byte BIP39 seed (operator's seed).
659 domain:
660 The domain to scope this agent to. The agent can only derive keys
661 within this domain from the returned sub-seed.
662 agent_id:
663 Agent slot index within the domain. Must be >= 0.
664
665 Returns
666 -------
667 bytes
668 64-byte domain-scoped sub-seed. Treat with the same care as the
669 master seed — never log it, store it unencrypted, or transmit over
670 an unauthenticated channel.
671
672 Raises
673 ------
674 HdKeyError
675 If *domain* or *agent_id* is negative.
676 Slip010Error
677 If *seed* is shorter than 16 bytes.
678
679 Security
680 --------
681 Each (domain, agent_id) pair produces a unique, independent sub-seed.
682 Granting multiple domain sub-seeds to an agent (for cross-domain
683 capability) is done at the orchestration layer (agentception) by injecting
684 multiple sub-seeds — never by combining them or using the master seed.
685
686 Examples
687 --------
688 ::
689
690 # Music composition agent — scoped to music domain only
691 music_seed = derive_agent_sub_seed(seed, domain=DOMAIN_MUSIC, agent_id=0)
692
693 # Same agent also needs to authenticate — inject identity sub-seed separately
694 auth_seed = derive_agent_sub_seed(seed, domain=DOMAIN_IDENTITY, agent_id=0)
695
696 # Agent uses each seed for its respective domain
697 agent_identity_dk = derive_identity_key(auth_seed)
698 agent_music_dk = derive_domain_key(music_seed, domain=DOMAIN_MUSIC)
699 """
700 _validate_non_negative(domain, "domain")
701 _validate_non_negative(agent_id, "agent_id")
702 # Root: m / purpose' / domain' / ENTITY_AGENT' / agent_id'
703 dk = master_key(seed)
704 for hardened_index in [hardened(MUSE_PURPOSE), hardened(domain), hardened(ENTITY_AGENT), hardened(agent_id)]:
705 next_dk = child_key(dk, hardened_index)
706 dk.zero()
707 dk = next_dk
708 sub_seed = SecretByteArray(dk.private_bytes) + bytearray(dk.chain_code)
709 dk.zero()
710 return SecretByteArray(sub_seed)
711
712 # ---------------------------------------------------------------------------
713 # Key materialisation helpers
714 # ---------------------------------------------------------------------------
715
716 def dk_to_ed25519(dk: DerivedKey) -> "Ed25519PrivateKey":
717 """Materialise a :class:`~muse.core.slip010.DerivedKey` as an Ed25519 signing key.
718
719 Thin re-export of :func:`muse.core.slip010.to_ed25519_private_key` so
720 callers can use a single import from this module::
721
722 from muse.core.hdkeys import derive_identity_key, dk_to_ed25519
723
724 Parameters
725 ----------
726 dk:
727 Derived key from any ``derive_*`` function in this module.
728
729 Returns
730 -------
731 Ed25519PrivateKey
732 ``cryptography`` library signing key. Call ``.sign(message)`` to
733 produce an Ed25519 signature, and ``.public_key().public_bytes_raw()``
734 for the 32-byte public key.
735
736 Examples
737 --------
738 ::
739
740 dk = derive_identity_key(seed)
741 priv = dk_to_ed25519(dk)
742 sig = priv.sign(b"hello muse")
743 priv.public_key().verify(sig, b"hello muse") # does not raise → valid
744 """
745 return to_ed25519_private_key(dk)
746
747 def public_bytes_from_seed(
748 seed: bytes,
749 domain: int = DOMAIN_IDENTITY,
750 entity_type: int = ENTITY_HUMAN,
751 entity_id: int = 0,
752 role: int = ROLE_SIGN,
753 index: int = 0,
754 ) -> bytes:
755 """Return the raw 32-byte Ed25519 public key at the given Muse coordinates.
756
757 Convenience one-liner for callers that only need the public key — e.g.
758 to display a fingerprint or register with MuseHub.
759
760 Parameters
761 ----------
762 seed:
763 64-byte BIP39 seed or agent sub-seed.
764 domain:
765 Domain index. Default: :data:`DOMAIN_IDENTITY`.
766 entity_type:
767 Entity class. Default: :data:`ENTITY_HUMAN`.
768 entity_id:
769 Entity slot. Default: ``0``.
770 role:
771 Key role. Default: :data:`ROLE_SIGN`.
772 index:
773 Rotation index. Default: ``0``.
774
775 Returns
776 -------
777 bytes
778 32-byte raw Ed25519 public key.
779
780 Examples
781 --------
782 ::
783
784 pub = public_bytes_from_seed(seed) # identity key public bytes
785 fingerprint = pub.hex()[:16] # short display fingerprint
786 assert len(pub) == 32
787 """
788 dk = derive_key(seed, domain, entity_type, entity_id, role, index)
789 try:
790 private_key = dk_to_ed25519(dk)
791 finally:
792 dk.zero()
793 return private_key.public_key().public_bytes_raw()
794
795 # ---------------------------------------------------------------------------
796 # Internal validators
797 # ---------------------------------------------------------------------------
798
799 def _validate_non_negative(value: int, name: str) -> None:
800 if value < 0:
801 raise HdKeyError(f"{name} must be >= 0; got {value}.")
802
803 def _validate_entity_type(entity_type: int) -> None:
804 if entity_type < 0 or entity_type > 2:
805 raise HdKeyError(
806 f"entity_type must be 0 (ENTITY_HUMAN), 1 (ENTITY_AGENT), "
807 f"or 2 (ENTITY_ORG); got {entity_type}."
808 )
809
810 def _validate_role(role: int) -> None:
811 if role < 0 or role > 4:
812 raise HdKeyError(
813 f"role must be 0 (ROLE_SIGN), 1 (ROLE_RECEIVE), 2 (ROLE_PROVISION), "
814 f"3 (ROLE_ATTEST), or 4 (ROLE_DELEGATE); got {role}."
815 )
File History 1 commit
sha256:2a703f78341332ef0beb9856d2267de6aec89b3883c31519b6900b667d026e62 chore: delete muse/prose domain — hallucinated, never existed Sonnet 4.6 minor 5 days ago