Class: AceMQ::AMQP::Keyring
- Inherits:
-
Object
- Object
- AceMQ::AMQP::Keyring
- Defined in:
- lib/acemq/amqp/crypto.rb
Overview
The keys a service can write with and the keys it can read with.
Those are not the same set, and that asymmetry is the whole of key rotation. A service writes with exactly one key — the current one — and must be able to read with every key any message still in flight was written with. Rotating means adding a key and making it current, while the old ones stay readable until nothing written with them is left.
keys = Keyring.new(EncryptionKey.new("orders-2026-09", now),
EncryptionKey.new("orders-2026-06", june))
The first key is the one that writes. The order rotation happens in is: add the new key everywhere first, so every consumer can read it, and only then make it current somewhere.
A keyring is anything answering current and key_for, so one backed by
a key management service is a small class rather than a fork of this one.
Implementations should cache: key_for is called for every message
decoded, and a key service charged per call will notice.
Class Method Summary collapse
- .check!(candidate) ⇒ Object private
-
.of(id, secret) ⇒ Keyring
A keyring holding one key, which both writes and reads.
Instance Method Summary collapse
-
#add(key) ⇒ Keyring
Puts a key on the ring without making it the one used for writing.
-
#current ⇒ EncryptionKey
The key new messages are written with.
-
#ids ⇒ Array<String>
The identifiers, for a health endpoint or a line at start-up.
-
#initialize(*keys) ⇒ Keyring
constructor
A new instance of Keyring.
- #inspect ⇒ Object
-
#key_for(id) ⇒ EncryptionKey
The key a message named.
- #to_s ⇒ Object
-
#use(id) ⇒ Keyring
Makes a key the one new messages are written with.
Constructor Details
#initialize(*keys) ⇒ Keyring
Returns a new instance of Keyring.
181 182 183 184 185 186 187 188 |
# File 'lib/acemq/amqp/crypto.rb', line 181 def initialize(*keys) raise ArgumentError, "a keyring needs at least one key" if keys.empty? @lock = Mutex.new @keys = {} keys.each { |key| add(key) } @current = keys.first.id end |
Class Method Details
.check!(candidate) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
199 200 201 202 203 204 |
# File 'lib/acemq/amqp/crypto.rb', line 199 def self.check!(candidate) return candidate if candidate.respond_to?(:current) && candidate.respond_to?(:key_for) raise ArgumentError, "#{candidate.inspect} is not a keyring: it needs current and key_for" end |
.of(id, secret) ⇒ Keyring
A keyring holding one key, which both writes and reads.
195 |
# File 'lib/acemq/amqp/crypto.rb', line 195 def self.of(id, secret) = new(EncryptionKey.new(id, secret)) |
Instance Method Details
#add(key) ⇒ Keyring
Puts a key on the ring without making it the one used for writing.
210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/acemq/amqp/crypto.rb', line 210 def add(key) unless key.is_a?(EncryptionKey) raise ArgumentError, "a keyring holds EncryptionKey, not #{key.class}" end @lock.synchronize do @keys[key.id] = key @current ||= key.id end self end |
#current ⇒ EncryptionKey
The key new messages are written with.
Consulted per message, so a keyring that reloads from a secret store can change what this returns and the next message uses the new key.
243 244 245 246 247 248 249 250 |
# File 'lib/acemq/amqp/crypto.rb', line 243 def current @lock.synchronize do key = @keys[@current] raise ConfigurationError, "this keyring has no current key" if key.nil? key end end |
#ids ⇒ Array<String>
The identifiers, for a health endpoint or a line at start-up. Never the keys.
279 |
# File 'lib/acemq/amqp/crypto.rb', line 279 def ids = @lock.synchronize { @keys.keys.sort } |
#inspect ⇒ Object
282 |
# File 'lib/acemq/amqp/crypto.rb', line 282 def inspect = "#<AceMQ::AMQP::Keyring #{self}>" |
#key_for(id) ⇒ EncryptionKey
The key a message named.
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 |
# File 'lib/acemq/amqp/crypto.rb', line 258 def key_for(id) @lock.synchronize do key = @keys[id.to_s] # Naming what is held is safe — identifiers travel in the clear # anyway — and it is the difference between "the key was retired # while messages were still queued" and "this message came from a # service using a different keyring", which have different fixes. if key.nil? raise DecodeError, "this message was encrypted with key #{id.to_s.inspect}, which is not on " \ "this keyring; it holds #{@keys.keys.sort.join(", ")}" end key end end |
#to_s ⇒ Object
281 |
# File 'lib/acemq/amqp/crypto.rb', line 281 def to_s = "Keyring{current=#{@lock.synchronize { @current }}, holds=#{ids.join(", ")}}" |
#use(id) ⇒ Keyring
Makes a key the one new messages are written with.
226 227 228 229 230 231 232 233 234 235 |
# File 'lib/acemq/amqp/crypto.rb', line 226 def use(id) @lock.synchronize do unless @keys.key?(id.to_s) raise ArgumentError, "there is no key #{id.to_s.inspect} on this keyring" end @current = id.to_s end self end |