Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AllowedSigners support #23

Merged
merged 16 commits into from
May 9, 2024
Merged

Conversation

timweri
Copy link
Collaborator

@timweri timweri commented Apr 28, 2024

This adds a new API to parse AllowedSigner. The specification can be found here: https://man.openbsd.org/ssh-keygen.1#ALLOWED_SIGNERS

The parser rejects unsupported options instead of building a HashMap to store additional options.

Comment on lines 85 to 147
for option in parts {
let option = option.to_lowercase();
let (key, value) = match option.split_once('=') {
Some(v) => v,
None => (option.as_str(), ""),
};
match key {
"cert-authority" => cert_authority = true,
"namespaces" => {
if namespaces.is_some() {
return Err(Error::InvalidFormat);
}
let namespaces_inner: Vec<&str> = value.trim_matches('"')
.split(',')
.collect();
namespaces = Some(
namespaces_inner.iter()
.map(|s| s.to_string())
.collect()
);
},
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TODO: Rework the option parsing logic:

  • Support spaces in double quotes. Not sure how this works for a namespace. Need to find the specs for namespace.
  • Might need to enforce a format for namespace.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The key used for signing is specified using the -f option and may refer to either a private key, or a public key with the private half available via ssh-agent(1). An additional signature namespace, used to prevent signature confusion across different domains of use (e.g. file signing vs email signing) must be provided via the -n flag. Namespaces are arbitrary strings, and may include: “file” for file signing, “email” for email signing. For custom uses, it is recommended to use names following a NAMESPACE@YOUR.DOMAIN pattern to generate unambiguous namespaces.

From https://man.openbsd.org/ssh-keygen.1

Comment on lines 72 to 74
let principals = parts.pop_front().ok_or(Error::InvalidFormat)?;
let principals: Vec<&str> = principals.split(',').collect();
let principals = principals.iter().map(|s| s.to_string()).collect();
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Do we enforce a format for principals?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Seems like this is not done in other APIs (cert.rs) so maybe we don't need this?

Copy link
Owner

Choose a reason for hiding this comment

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

I think principals are pretty open. There might be characters which cannot be in principals (","?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm not enforcing format on principals

/// Please refer to [ssh-keygen-1.ALLOWED_SIGNERS] for more details about the format.
/// [ssh-keygen-1.ALLOWED_SIGNERS]: https://man.openbsd.org/ssh-keygen.1#ALLOWED_SIGNERS
#[derive(Debug, PartialEq, Eq)]
pub struct AllowedSigners {
Copy link
Owner

Choose a reason for hiding this comment

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

You might just want to use a typedef here (I don't think there would ever be additional fields in this struct.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I can't implement foreign traits on typedef since typedef is a weak symlink. So I chose to wrap AllowedSigners(Vec<AllowedSigner>)

Copy link
Owner

@obelisk obelisk left a comment

Choose a reason for hiding this comment

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

Few things around parsing

@timweri timweri requested a review from obelisk May 3, 2024 20:47
}

// Remove everything from the first #
if let Some(line) = line.split('#').next() {
Copy link
Owner

Choose a reason for hiding this comment

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

This will break for namespaces which contain pound

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It was intended. But I checked and git's allowed_signers allows # in namespaces. I updated the parsing logic.

@timweri timweri force-pushed the add-allowed-signers branch from 1487bc2 to b9b2e2e Compare May 4, 2024 02:32
let (mut head, mut rest) = s.split_once(char::is_whitespace)
.ok_or(Error::InvalidAllowedSigner("missing key data".to_string()))?;

let principals: Vec<&str> = head.split(',').collect();
Copy link
Owner

Choose a reason for hiding this comment

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

Since you just realloc as String below, you may as well make this Vec and get the allocations done in one step.


let principals: Vec<&str> = head.split(',').collect();
if principals.iter().any(|p| p.is_empty()) {
return Err(Error::InvalidAllowedSigner("principal cannot be empty".to_string()));
Copy link
Owner

Choose a reason for hiding this comment

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

I think you can have a sub enum to make this more clear and get rid of the static strings from here (they'll move to the std::fmt::Display of the new enum type)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done!

src/ssh/allowed_signer.rs Outdated Show resolved Hide resolved
src/ssh/allowed_signer.rs Outdated Show resolved Hide resolved
src/ssh/allowed_signer.rs Outdated Show resolved Hide resolved
src/ssh/allowed_signer.rs Outdated Show resolved Hide resolved
src/ssh/allowed_signer.rs Show resolved Hide resolved
src/ssh/allowed_signer.rs Show resolved Hide resolved
@timweri
Copy link
Collaborator Author

timweri commented May 6, 2024

Indicates that the key is valid for use at or after the specified timestamp, which may be a date or time in the YYYYMMDD[Z] or YYYYMMDDHHMM[SS][Z] formats. Dates and times will be interpreted in the current system time zone unless suffixed with a Z character, which causes them to be interpreted in the UTC time zone.

I just realized the timestamp specs is YYYYMMDD[HHMM[SS]][Z]. I can parse this format from scratch, but then I still need to validate if a date is valid (eg. 30 Feb). So I imported chrono. I'm also open to not validating the timestamp, and just returns the timestamp as is so we don't have to include another external crate.

Another problem is UTC vs Local timezone. Right now, I return the local timezone timestamp as i64. If it's UTC, I convert it to local timezone. Not sure how to test the UTC -> Local timezone conversion in the unittest since Local timezone is not a static timezone.

@timweri timweri requested a review from obelisk May 6, 2024 05:41

impl AllowedSignerSplitter {
/// Split the string by delimiters but keep the delimiters.
pub(in self) fn new(s: &str) -> Self {
Copy link
Owner

Choose a reason for hiding this comment

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

Is this different from pub(self)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No. I changed it to pub(self)

// If we don't see any double quote in the token, we greedily parse the token until the
// next whitespace.
let mut s = String::new();
while !self.buffer.is_empty()
Copy link
Owner

Choose a reason for hiding this comment

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

You might be able to get rid of the the last().unwrap() by using a while let here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've used while let where I can. I also tried to use if let to remove the unwrap but that made things look a little uglier since if let _ && cond is still unstable.

/// The timestamp can be enclosed by quotation marks.
fn parse_timestamp(s: &str) -> Result<i64> {
let mut s = s.trim_matches('"');
println!("s: {}", s);
Copy link
Owner

Choose a reason for hiding this comment

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

Rogue println

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed

Copy link
Owner

@obelisk obelisk left a comment

Choose a reason for hiding this comment

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

Just a couple comments. Mostly I'm worried about panics in the splitter due to extensive use of [] and unwrap.

@timweri timweri requested a review from obelisk May 7, 2024 20:10
@timweri
Copy link
Collaborator Author

timweri commented May 7, 2024

I've got rid of all the unwrap I can. The remaining ones are only initialization/conversion calls from constants that should not fail.

Anyway, could you approve the GHA workflow to run the tests?

@timweri timweri force-pushed the add-allowed-signers branch 2 times, most recently from ea1f496 to e655af2 Compare May 8, 2024 23:10
@obelisk obelisk merged commit 2b5106e into obelisk:main May 9, 2024
5 checks passed
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