2

Currently looking into OpenPubKey and more specifically into OpenPubkey SSH:

Terminology:

  • OPK => OpenPubkey
  • OIDC => OpenID Connect
  • OP => OpenID provider

I still have some problems understanding the details. My understanding of the workflow for OPK SSH is the following:

  • OPK client calls OPK SSH client executable, detailed steps below:

    • OPK client generates new keypair for Alice (private/public)
    • OPK client computes nonce (SHA3_256(upk=alice-pubkey, alg=ES256, rz=crypto.Rand()))
    • OPK client initiates OIDC authentication with OP (e.g. google.com) and sends along above computed nonce
    • OP asks for Alice's consent and credentials
    • Alice accepts and successfully authenticates to OP
    • OP sends back ID Token (including the nonce and identity information) signed by OP (google.com private key)

    NOT SURE ABOUT ALL STEPS BELOW

    • OPK Client creates SSH certificate:
      • Puts alice-pubkey in pubkey field
      • Puts ID token and the random value (generated before, part of nonce) in Subject field
      • ...other fields
      • Includes signature (signed SSH certificate fields, signed by alice-privkey) => is this correct?
    • OPK Client connects to OPK SSH Server via SSH by presenting SSH certificate
  • OPK SSH Server receives SSH certificate and forwards it to OPK SSH server executable (specified for AuthorizedKeysCommand):

    • OPK SSH Server extracts alice-pubkey from SSH certificate
    • OPK SSH Server checks SSH certificate signature with alice-pubkey
    • OPK SSH Server gets OP pubkey (publicly available), extracts ID Token and checks its signature (must be signed by OP)
    • OPK SSH Server extracts email from ID Token and checks in OPK SSH config (policies.yaml) if that user should be granted access
      • If the user has access, continue with steps below, otherwise deny access
    • OPK SSH Server computes nonce with random number and alice-pubkey (both extracted from SSH cert)
    • OPK SSH Server compares nonce computed above with nonce from ID Token
      • If they match, user can login

Can you guys please have a look at that workflow and maybe add/correct things if necessary? I have not found a detailed OpenPubkey SSH workflow anywhere, so this could possibly be beneficial in avoiding a lot of future questions regarding the inner workings of OpenPubkey SSH as soon as it gets more popular.

Please also add correct/anything regarding the terminology used.

Thanks in advance :)

1
  • Thanks for this excellent question. Let me know if you have any other OpenPubkey questions Mar 21 at 15:08

1 Answer 1

3

Creating a detailed workflow of how SSH works with OpenPubkey is an excellent idea! I hope transform my answer here into documentation.

If you really want to deep dive into how this works we have a fairly easy to read code example of SSH using OpenPubkey and the PR that I wrote, which adds SSH provides additional diagrams and documentation. If you come across any bugs or want to add any improvements, we welcome all contributors. Feel free to open a PR.

Creating PK Token

In your "OPK client calls OPK SSH client executable" steps, you are missing the final step the OPK Client performs which is:

  • After the OPK Client receives the ID Token, the OPK Client uses Alice key to sign the ID Token and adds this additional signature to the ID Token. We call this ID Token with Alice's signature: the PK Token.

ID Tokens are JWS (JSON Web Signatures). JWS can have more than one signature/protected header. An ID Token in JWS format looks like:

ID Token

{"payload": 
   {"sub":"1234...", "iss":"https://accounts.google.com", "email": "[email protected]", 
    "nonce": Hash(upk=alice-pubkey, alg=ES256, rz=crypto.Rand(), typ="CIC")},
"signatures": [
  {
  "protected": {"typ": "JWT", "alg": "RS256", "kid": "abcd...", "typ": "JWT"},
  "signature": SIGN(google-signkey, (payload, signatures[0].protected))
  }
]}

We simply add Alice's signature on the payload and Alice's protected header to the ID Token, creating a PK Token as shown:

{"payload": 
   {"sub":"1234...", "iss":"https://accounts.google.com", "email": "[email protected]", 
    "nonce": Hash(upk=alice-pubkey, alg=ES256, rz=crypto.Rand(), typ="CIC")},
"signatures": [
  {
  "protected": {"typ": "JWT", "alg": "RS256", "kid": "abcd...", "typ": "JWT"},
  "signature": SIGN(google-signkey, (payload, signatures[0].protected))
  },
  {"protected": {"upk": alice-pubkey, "alg": "EC256", "rz": crypto.Rand(), "typ": "CIC"},
  "signature": <SIGN(alice-signkey, (payload, signatures[1].protected))>
  },
]}

This is important for two reasons:

  1. The value in the nonce is the hash of Alice's protected header. That means anyone with the PK Token can determine Alice's public key without any additional information.
  2. Alice's signature functions as a Proof of Possession, preventing rogue key attacks that have plagued similar systems.

Creating OpenPubkey SSH Public Key/SSH Certificate

OpenPubkey SSH flow

Your steps for "OPK Client creates SSH certificate" are mostly correct, but there are some missing pieces. Let me rewrite those steps:

  • OPK Client creates SSH certificate:
    • Puts alice-pubkey in pubkey field
    • Puts user's email address (from ID Token/PK Token payload) in keyId field
    • Puts PK Token in certificate extension field named openpubkey-pkt
    • OPK Client signs SSH certificate with Alice's signing key (a.k.a. private key)
    • OPK Client saves the SSH certificate as a default SSH key in the default SSH directory as either ~/.ssh/id_ecdsa or ~/.ssh/id_dsa. This works because SSH sees SSH certificates as a type of SSH key.

The code for this is simple:

SshCert: &ssh.Certificate{
    Key:             pubkeySsh,
    CertType:        ssh.UserCert,
    KeyId:           claims.Email,
    ValidPrincipals: principals,
    ValidBefore:     ssh.CertTimeInfinity,
    Permissions: ssh.Permissions{
        Extensions: map[string]string{
            "permit-X11-forwarding":   "",
            "permit-agent-forwarding": "",
            "permit-port-forwarding":  "",
            "permit-pty":              "",
            "permit-user-rc":          "",
            "openpubkey-pkt":          pktB64,
        },
    },
},
  • User runs ssh user@hostname.

    • The SSH command sends all default ssh keys in ~/.ssh/ to the SSH server. Thus, the SSH certificate we created is sent the ssh server. This works on Windows, OSX, and Linux.
  • SSH server receives all the SSH public keys sent from the SSH client and processes them according to its /etc/sshd_config

    • In /etc/sshd_config, we have added an AuthorizedKeysCommand. The SSH server will pass all SSH public keys sent by the SSH client to the program listed in the AuthorizedKeysCommand, which in our case is AuthorizedKeysCommand /etc/opk/opkssh ver %u %k %t.
      • %u is the username the SSH client wants to assume.
      • %t is the base64 encoding of the SSH public key/certificate.
    • This calls /etc/opk/opkssh which in turn:
      • Checks if email is allowed to assume user name according the policy.
      • Checks if public key in SSH certificate matches Alice's public key in SSH Certificate
      • That PK Token is valid according to the rules of OpenPubkey. This includes checking if ID Token is unexpired and is validly signed by the OpenID Provider (OP), e.g. Google. Additionally it checks that [the nonce claim in the PK token is the same as the hash of the PK token's protected header containing alice-pubkey and the random number][8].
      • If all of these checks pass, /etc/opk/opkssh tells the SSH server to allow the SSH connection as the specified user.

Additional notes

Includes signature (signed SSH certificate fields, signed by alice-privkey) => is this correct?

Yes, this is needed to prove the user actually created the ssh certificate and didn't just plug someone else's PK Token into the SSH certificate. The security impact of not signing the SSH certificate would be small; the SSH connection would still fail when the user attempts to perform an SSH handshake without the correct public key. Better to fail as early as possible and head off any shenanigans at the pass.

For the purposes of OpenPubkey, we just want AuthorizedKeyCommand to be able to say if the Alice's SSH public key (which happens to be an SSH certificate) is valid or invalid. The semantics of AuthorizedKeyCommand require that we can't just return true, but instead we are required to return a public key. The way our AuthorizedKeyCommand verifier signals to the ssh-server that Alice's key is valid, is that it returns the public key which Alice's key was signed under. In our currently design this happens to be the user's public key, but we could have build it so that it is just a public value or a CA.

For instance if you already have a CA that issues SSH Certificates, you can just use that instead of Alice's key and then OpenPubkey certificates would be backwards compatible with your existing ssh configurations. It allows you to do partial deployments. See our code which is called by AuthorizedKeyCommand

// This function is called by the SSH server as the authorizedKeysCommand:
//
// The following lines are added to /etc/ssh/sshd_config:
//
//  AuthorizedKeysCommand /etc/opk/opkssh ver %u %k %t
//  AuthorizedKeysCommandUser root
//
// The parameters specified in the config map the parameters sent to the function below.
// We prepend "Arg" to specify which ones are arguments sent by sshd. They are:
//
//  %u The username (requested principal) - userArg
//  %t The public key type - typArg - in this case a certificate being used as a public key
//  %k The base64-encoded public key for authentication - certB64Arg - the public key is also a certificate
func authorizedKeysCommand(userArg string, typArg string, certB64Arg string, policyEnforcer policyCheck, op client.OpenIdProvider) (string, error) {
    cert, err := sshcert.NewFromAuthorizedKey(typArg, certB64Arg)
    if err != nil {
        return "", err
    }
    if pkt, err := cert.VerifySshPktCert(op); err != nil {
        return "", err
    } else if err := policyEnforcer(userArg, pkt); err != nil {
        return "", err
    } else {
        // sshd expects the public key in the cert, not the cert itself.
        // This public key is key of the CA the signs the cert, in our
        // setting there is no CA.
        pubkeyBytes := ssh.MarshalAuthorizedKey(cert.SshCert.SignatureKey)
        return "cert-authority " + string(pubkeyBytes), nil
    }
}

OpenPubkey just uses plain SSH (no code changes, two lines of config in sshd_config)

Regarding the OPK SSH server, what you wrote is technically accurate but I worry calling it a "server" might confuse a reader less familiar with OpenPubkey.

The SSH server is just a standard OpenSSH server configured to work with OpenPubkey PK Tokens. The SSH client is just the SSH client you have installed. opk-ssh just creates an SSH key pair with a PK Token. What you are calling the OPK SSH server is just a very simple command called by that actual SSH server. It isn't a server.

[8]: func (v *DefaultProviderVerifier) verifyCommitment(pkt *pktoken.PKToken) error {

13
  • 1
    Thank you for the very detailed answer @Ethan, this is really detailed and helpful, also the PR link you provided is excellent.
    – ieggel
    Mar 22 at 10:45
  • 1
    I am still not sure about the very last step. So if I understand right, the AuthorizedKeysCommand returns the CA pub key (in this case alice-pubkey => cert-authority <alice-pubkey>), which is used to verify the SSH certificate's signature (signed by alice-privkey)? You said 'The security impact of not signing the ssh certificate would be small, the ssh connection would still fail when the user attempts to perform an ssh handshake without the correct public key'. I don't quite get it, would you mind explaining?
    – ieggel
    Mar 22 at 11:34
  • 1
    "I hope transform my answer here into documentation." Did you mean "I hope to transform" or "I hope they transform" ? Mar 22 at 15:22
  • 1
    @ieggel The AuthorizedKeyCommand job here is to take the SSH certificate to check if the SSH Certificate is valid or not. If the SSH Certificate is valid, AuthorizedKeyCommand returns a public key that will verify the SSH Certificate to signal to the ssh-server that it is valid. For our purposes AuthorizedKeyCommand would just work as well if it returns "true or false." Let me try to edit this better into the answer. Mar 23 at 15:44
  • 1
    @Ethan Heilman: Thanks. This is how I understood it, but I was not sure. Your modified answer makes is crystal clear now. Thanks for your support. Also thanks to Mike in your team for helping me with the installation, as there were some issues with the installation binary. I tested everything and it works flawlessly. Great job.
    – ieggel
    Mar 25 at 13:10

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .