require 'securerandom'
module Net
module SSH
module Authentication
# Class for representing an SSH certificate.
#
# http://cvsweb.openbsd.org/cgi-bin/cvsweb/~checkout~/src/usr.bin/ssh/PROTOCOL.certkeys?rev=1.10&content-type=text/plain
class Certificate
attr_accessor :nonce
attr_accessor :key
attr_accessor :serial
attr_accessor :type
attr_accessor :key_id
attr_accessor :valid_principals
attr_accessor :valid_after
attr_accessor :valid_before
attr_accessor :critical_options
attr_accessor :extensions
attr_accessor :reserved
attr_accessor :signature_key
attr_accessor :signature
# Read a certificate blob associated with a key of the given type.
def self.read_certblob(buffer, type)
cert = Certificate.new
cert.nonce = buffer.read_string
cert.key = buffer.read_keyblob(type)
cert.serial = buffer.read_int64
cert.type = type_symbol(buffer.read_long)
cert.key_id = buffer.read_string
cert.valid_principals = buffer.read_buffer.read_all(&:read_string)
cert.valid_after = Time.at(buffer.read_int64)
cert.valid_before = if RUBY_PLATFORM == "java"
# 0x20c49ba5e353f7 = 0x7fffffffffffffff/1000, the largest value possible for JRuby
# JRuby Time.at multiplies the arg by 1000, and then stores it in a signed long.
# 0x20c49ba2d52500 = 292278993-01-01 00:00:00 +0000
# JRuby 9.1 does not accept the year 292278994 because of edge cases (https://github.com/JodaOrg/joda-time/issues/190)
Time.at([0x20c49ba2d52500, buffer.read_int64].min)
else
Time.at(buffer.read_int64)
end
cert.critical_options = read_options(buffer)
cert.extensions = read_options(buffer)
cert.reserved = buffer.read_string
cert.signature_key = buffer.read_buffer.read_key
cert.signature = buffer.read_string
cert
end
def ssh_type
key.ssh_type + "-cert-v01@openssh.com"
end
def ssh_signature_type
key.ssh_type
end
# Serializes the certificate (and key).
def to_blob
Buffer.from(
:raw, to_blob_without_signature,
:string, signature
).to_s
end
def ssh_do_sign(data, sig_alg = nil)
key.ssh_do_sign(data, sig_alg)
end
def ssh_do_verify(sig, data, options = {})
key.ssh_do_verify(sig, data, options)
end
def to_pem
key.to_pem
end
def fingerprint
key.fingerprint
end
# Signs the certificate with key.
def sign!(key, sign_nonce = nil)
# ssh-keygen uses 32 bytes of nonce.
self.nonce = sign_nonce || SecureRandom.random_bytes(32)
self.signature_key = key
self.signature = Net::SSH::Buffer.from(
:string, key.ssh_signature_type,
:mstring, key.ssh_do_sign(to_blob_without_signature)
).to_s
self
end
def sign(key, sign_nonce = nil)
cert = clone
cert.sign!(key, sign_nonce)
end
# Checks whether the certificate's signature was signed by signature key.
def signature_valid?
buffer = Buffer.new(signature)
sig_format = buffer.read_string
signature_key.ssh_do_verify(buffer.read_string, to_blob_without_signature, host_key: sig_format)
end
def self.read_options(buffer)
names = []
options = buffer.read_buffer.read_all do |b|
name = b.read_string
names << name
data = b.read_string
data = Buffer.new(data).read_string unless data.empty?
[name, data]
end
raise ArgumentError, "option/extension names must be in sorted order" if names.sort != names
Hash[options]
end
private_class_method :read_options
def self.type_symbol(type)
types = { 1 => :user, 2 => :host }
raise ArgumentError("unsupported type: #{type}") unless types.include?(type)
types.fetch(type)
end
private_class_method :type_symbol
private
def type_value(type)
types = { user: 1, host: 2 }
raise ArgumentError("unsupported type: #{type}") unless types.include?(type)
types.fetch(type)
end
def ssh_time(t)
# Times in certificates are represented as a uint64.
[[t.to_i, 0].max, 2 << 64 - 1].min
end
def to_blob_without_signature
Buffer.from(
:string, ssh_type,
:string, nonce,
:raw, key_without_type,
:int64, serial,
:long, type_value(type),
:string, key_id,
:string, valid_principals.inject(Buffer.new) { |acc, elem| acc.write_string(elem) }.to_s,
:int64, ssh_time(valid_after),
:int64, ssh_time(valid_before),
:string, options_to_blob(critical_options),
:string, options_to_blob(extensions),
:string, reserved,
:string, signature_key.to_blob
).to_s
end
def key_without_type
# key.to_blob gives us e.g. "ssh-rsa,<key>" but we just want "<key>".
tmp = Buffer.new(key.to_blob)
tmp.read_string # skip the underlying key type
tmp.read
end
def options_to_blob(options)
options.keys.sort.inject(Buffer.new) do |b, name|
b.write_string(name)
data = options.fetch(name)
data = Buffer.from(:string, data).to_s unless data.empty?
b.write_string(data)
end.to_s
end
end
end
end
end