File: issuer.rb

package info (click to toggle)
ruby-localhost 1.7.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 232 kB
  • sloc: ruby: 579; makefile: 4
file content (152 lines) | stat: -rw-r--r-- 4,370 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.

require "openssl"
require "fileutils"

require_relative "state"

module Localhost
	# Represents a local Root Certificate Authority used to sign development certificates.
	class Issuer
		# The default number of bits for the private key. 4096 bits.
		BITS = 4096
		
		# The default validity period for the certificate. 10 years in seconds.
		VALIDITY = 10 * 365 * 24 * 60 * 60
		
		# Fetch (load or create) a certificate issuer with the given name.
		# See {#initialize} for the format of the arguments.
		def self.fetch(*arguments, **options)
			issuer = self.new(*arguments, **options)
			
			unless issuer.load
				issuer.save
			end
			
			return issuer
		end
		
		# The default certificate issuer name.
		NAME = "development"
		
		# Initialize the issuer with the given name.
		# 
		# @parameter name [String] The common name to use for the certificate.
		# @parameter path [String] The path path for loading and saving the certificate.
		def initialize(name = nil, path: State.path)
			@name = name || NAME
			@path = path
			
			@subject = nil
			@key = nil
			@certificate = nil
		end
		
		# @returns [String] The path to the private key.
		def key_path
			File.join(@path, "#{@name}.key")
		end
		
		# @returns [String] The path to the public certificate.
		def certificate_path
			File.join(@path, "#{@name}.crt")
		end
		
		# @returns [OpenSSL::X509::Name] The subject name for the certificate.
		def subject
			@subject ||= OpenSSL::X509::Name.parse("/O=localhost.rb/CN=#{@name}")
		end
		
		# Set the subject name for the certificate.
		#
		# @parameter subject [OpenSSL::X509::Name] The subject name for the certificate.
		def subject= subject
			@subject = subject
		end
		
		# @returns [OpenSSL::PKey::RSA] The private key.
		def key
			@key ||= OpenSSL::PKey::RSA.new(BITS)
		end
		
		# The public certificate.
		#
		# @returns [OpenSSL::X509::Certificate] A self-signed certificate.
		def certificate
			@certificate ||= OpenSSL::X509::Certificate.new.tap do |certificate|
				certificate.subject = self.subject
				# We use the same issuer as the subject, which makes this certificate self-signed:
				certificate.issuer = self.subject
				
				certificate.public_key = self.key.public_key
				
				certificate.serial = Time.now.to_i
				certificate.version = 2
				
				certificate.not_before = Time.now - 10
				certificate.not_after = Time.now + VALIDITY
				
				extension_factory = ::OpenSSL::X509::ExtensionFactory.new
				extension_factory.subject_certificate = certificate
				extension_factory.issuer_certificate = certificate
				
				certificate.add_extension extension_factory.create_extension("basicConstraints", "CA:TRUE", true)
				certificate.add_extension extension_factory.create_extension("keyUsage", "keyCertSign, cRLSign", true)
				certificate.add_extension extension_factory.create_extension("subjectKeyIdentifier", "hash")
				certificate.add_extension extension_factory.create_extension("authorityKeyIdentifier", "keyid:always", false)
				
				certificate.sign self.key, OpenSSL::Digest::SHA256.new
			end
		end
		
		# Load the certificate and key from the given path.
		#
		# @parameter path [String] The path to load the certificate and key.
		# @returns [Boolean] True if the certificate and key were loaded successfully.
		def load(path = @root)
			certificate_path = self.certificate_path
			key_path = self.key_path
			
			return false unless File.exist?(certificate_path) and File.exist?(key_path)
			
			certificate = OpenSSL::X509::Certificate.new(File.read(certificate_path))
			key = OpenSSL::PKey::RSA.new(File.read(key_path))
			
			@certificate = certificate
			@key = key
			
			return true
		end
		
		# @returns [String] The path to the lockfile.
		def lockfile_path
			File.join(@path, "#{@name}.lock")
		end
		
		# Save the certificate and key to the given path.
		#
		# @parameter path [String] The path to save the certificate and key.
		def save(path = @root)
			lockfile_path = self.lockfile_path
			
			File.open(lockfile_path, File::RDWR|File::CREAT, 0644) do |lockfile|
				lockfile.flock(File::LOCK_EX)
				
				File.write(
					self.certificate_path,
					self.certificate.to_pem
				)
				
				File.write(
					self.key_path,
					self.key.to_pem
				)
			end
			
			return true
		end
	end
end