| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package ssh |
| |
| import ( |
| "context" |
| |
| "github.com/hashicorp/vault/api" |
| "github.com/hashicorp/vault/sdk/framework" |
| "github.com/hashicorp/vault/sdk/logical" |
| ) |
| |
| func pathVerify(b *backend) *framework.Path { |
| return &framework.Path{ |
| Pattern: "verify", |
| DisplayAttrs: &framework.DisplayAttributes{ |
| OperationPrefix: operationPrefixSSH, |
| OperationVerb: "verify", |
| OperationSuffix: "otp", |
| }, |
| Fields: map[string]*framework.FieldSchema{ |
| "otp": { |
| Type: framework.TypeString, |
| Description: "[Required] One-Time-Key that needs to be validated", |
| }, |
| }, |
| Callbacks: map[logical.Operation]framework.OperationFunc{ |
| logical.UpdateOperation: b.pathVerifyWrite, |
| }, |
| HelpSynopsis: pathVerifyHelpSyn, |
| HelpDescription: pathVerifyHelpDesc, |
| } |
| } |
| |
| func (b *backend) getOTP(ctx context.Context, s logical.Storage, n string) (*sshOTP, error) { |
| entry, err := s.Get(ctx, "otp/"+n) |
| if err != nil { |
| return nil, err |
| } |
| if entry == nil { |
| return nil, nil |
| } |
| |
| var result sshOTP |
| if err := entry.DecodeJSON(&result); err != nil { |
| return nil, err |
| } |
| |
| return &result, nil |
| } |
| |
| func (b *backend) pathVerifyWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { |
| otp := d.Get("otp").(string) |
| |
| // If OTP is not a UUID and a string matching VerifyEchoRequest, then the |
| // response will be VerifyEchoResponse. This is used by agent to check if |
| // connection to Vault server is proper. |
| if otp == api.VerifyEchoRequest { |
| return &logical.Response{ |
| Data: map[string]interface{}{ |
| "message": api.VerifyEchoResponse, |
| }, |
| }, nil |
| } |
| |
| // Create the salt of OTP because entry would have been create with the |
| // salt and not directly of the OTP. Salt will yield the same value which |
| // because the seed is the same, the backend salt. |
| salt, err := b.Salt(ctx) |
| if err != nil { |
| return nil, err |
| } |
| otpSalted := salt.SaltID(otp) |
| |
| // Return nil if there is no entry found for the OTP |
| otpEntry, err := b.getOTP(ctx, req.Storage, otpSalted) |
| if err != nil { |
| return nil, err |
| } |
| if otpEntry == nil { |
| return logical.ErrorResponse("OTP not found"), nil |
| } |
| |
| // Delete the OTP if found. This is what makes the key an OTP. |
| err = req.Storage.Delete(ctx, "otp/"+otpSalted) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Return username and IP only if there were no problems uptill this point. |
| return &logical.Response{ |
| Data: map[string]interface{}{ |
| "username": otpEntry.Username, |
| "ip": otpEntry.IP, |
| "role_name": otpEntry.RoleName, |
| }, |
| }, nil |
| } |
| |
| const pathVerifyHelpSyn = ` |
| Validate the OTP provided by Vault SSH Agent. |
| ` |
| |
| const pathVerifyHelpDesc = ` |
| This path will be used by Vault SSH Agent running in the remote hosts. The OTP |
| provided by the client is sent to Vault for validation by the agent. If Vault |
| finds an entry for the OTP, it responds with the username and IP it is associated |
| with. Agent uses this information to authenticate the client. Vault deletes the |
| OTP after validating it once. |
| ` |