Skip to content

feat(ssh): add rio command to generate ssh certificates#503

Open
rrkumarshikhar wants to merge 1 commit intodevelfrom
feat/rapyuta-ca
Open

feat(ssh): add rio command to generate ssh certificates#503
rrkumarshikhar wants to merge 1 commit intodevelfrom
feat/rapyuta-ca

Conversation

@rrkumarshikhar
Copy link
Copy Markdown
Contributor

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 12, 2026

🤖 Pull Request Artifacts (#24143739168) 🎉

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new rio ssh CLI command to request a signed SSH user certificate via the v2 SDK, write it next to the user’s SSH key, and optionally load it into ssh-agent for immediate use (issue #1550).

Changes:

  • Introduces riocli.ssh command implementation plus supporting utilities for key discovery, cert writing, agent loading, and expiry parsing.
  • Wires the new command into the main CLI bootstrap so it’s available as rio ssh.
  • Adds unit/integration-style tests for the new utilities and CLI flow, and updates dependencies to consume a specific SDK v2 Git revision.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
riocli/ssh/__init__.py Implements the rio ssh Click command: discover/read key, call SDK signing API, write cert, optionally add to ssh-agent.
riocli/ssh/util.py Adds helper functions for SSH key discovery, certificate path derivation, file writing, ssh-agent interaction, and expiry parsing.
riocli/bootstrap.py Registers the new ssh command with the root CLI.
tests/main/test_ssh.py Adds tests for utility functions and main CLI behaviors (happy path, flags, and error handling).
pyproject.toml Switches rapyuta-io-sdk-v2 dependency to a specific Git commit URL.
uv.lock Updates the lockfile to reflect the Git-sourced rapyuta-io-sdk-v2 dependency.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 56 to 62
"waiting>=1.4.1",
"yaspin==2.5.0",
"ansible-core>=2.13.13",
"rapyuta-io-sdk-v2>=0.5.0",
"rapyuta-io-sdk-v2 @ git+https://github.com/rapyuta-robotics/rapyuta-io-sdk-v2.git@8cd72e0a34d54d66990cc90109b232ec28ceefa9",
"typing-extensions>=4.15.0",
"python-benedict==0.30",
]
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.

Comment on lines +133 to +139
# 9. valid_before (uint64) — this is what we want
valid_before, _ = _read_uint64(cert_blob, pos)

return datetime.fromtimestamp(valid_before, tz=timezone.utc)

except Exception:
return None
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse_cert_valid_before() blindly converts the valid_before uint64 to a Python datetime via fromtimestamp(). OpenSSH certificates can encode “never expires” as valid_before = 0xFFFFFFFFFFFFFFFF, which will overflow fromtimestamp() and currently causes parsing to return None (and the cert to be treated as invalid). Consider explicitly handling the sentinel value (e.g., return datetime.max or a dedicated sentinel and have is_cert_valid() treat it as valid).

Copilot uses AI. Check for mistakes.
message = struct.pack(">IB", len(payload) + 1, msg_type) + payload
sock.sendall(message)

resp_len = struct.unpack(">I", _recv_all(sock, 4))[0]
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_send_recv() assumes the agent response length is at least 1 byte (for the response type) and immediately indexes resp[0]. If the agent returns an empty response (or the connection is truncated), this will raise an IndexError rather than a clear RuntimeError. Add an explicit guard for resp_len < 1 / empty resp and raise a protocol/connection error instead.

Suggested change
resp_len = struct.unpack(">I", _recv_all(sock, 4))[0]
resp_len = struct.unpack(">I", _recv_all(sock, 4))[0]
if resp_len < 1:
raise RuntimeError("SSH agent returned an empty response")

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +153
private_key = load_ssh_private_key(
private_key_path.read_bytes(),
password=None,
)

raw_pk = private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
raw_seed = private_key.private_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption(),
)

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_get_ed25519_raw_keys() implicitly assumes the private key at private_key_path is an unencrypted ed25519 key. If the file is missing, encrypted, or a different key type, load_ssh_private_key() / public_bytes(...Raw...) will raise cryptography exceptions that bubble up with low-level messages. Consider catching these cases and raising a RuntimeError with a clear, user-facing explanation (and validate that raw_seed and raw_pk are 32 bytes each before building the agent payload).

Suggested change
private_key = load_ssh_private_key(
private_key_path.read_bytes(),
password=None,
)
raw_pk = private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
raw_seed = private_key.private_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption(),
)
try:
private_key_data = private_key_path.read_bytes()
except OSError as exc:
raise RuntimeError(
f"Failed to read SSH private key: {private_key_path}"
) from exc
try:
private_key = load_ssh_private_key(
private_key_data,
password=None,
)
raw_pk = private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
raw_seed = private_key.private_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption(),
)
except (TypeError, ValueError) as exc:
raise RuntimeError(
"SSH private key must be an unencrypted Ed25519 private key: "
f"{private_key_path}"
) from exc
if len(raw_seed) != 32 or len(raw_pk) != 32:
raise RuntimeError(
"SSH private key did not produce valid Ed25519 raw key material "
f"(expected 32-byte seed and public key): {private_key_path}"
)

Copilot uses AI. Check for mistakes.
Comment on lines +289 to +295
try:
if public_key_path.is_file():
pk_blob = _get_key_blob(public_key_path)
remove_identity(pk_blob)
remove_identity(pk_blob)
except Exception:
pass
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove_rio_identities() currently wraps everything in except Exception: pass, which can mask unexpected programmer errors (and can make failures very hard to debug). It would be safer to only suppress the specific, expected failures here (e.g., agent unreachable, invalid/missing key file, base64 decode issues) and let other exceptions surface.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants