Class: Ksef::Crypto::PublicKeys
- Inherits:
-
Object
- Object
- Ksef::Crypto::PublicKeys
- Defined in:
- lib/ksef/crypto/public_keys.rb
Overview
The Ministry's published encryption certificates, fetched and cached (docs/REFERENCE.md §10.2).
GET /security/public-key-certificates is unauthenticated — the contract
declares no security for it — so this needs no credential and can run before any
authentication.
Why there is a TTL, and why it is not "forever"
Two rotation modes exist and must not be conflated. Re-certification issues a new
certificate over the same key pair, leaving publicKeyId unchanged. Key rotation
changes the key pair, so publicKeyId changes with it; a planned rotation publishes
the successor early and both appear for one usage during the overlap, while an
emergency rotation revokes the old key and drops it from the list immediately.
Because the emergency case can happen at any moment, the list must not be cached
indefinitely — and #with_key_rotation implements the documented recovery for the
window between a rotation and the cache expiring.
Thread-safe: one mutex guards the cache, and a Ksef::Client is required to be
shareable across threads (DESIGN.md §5.2).
Constant Summary collapse
- PATH =
"security/public-key-certificates"- DEFAULT_TTL =
An hour. Long enough that the list is fetched once per process in practice, short enough to bound how long a withdrawn key can linger. Callers who want none of it can pass
ttl: 0. 3600- UNKNOWN_KEY_CODE =
400with this code means "the supplied key identifier is unknown or refers to a withdrawn key". The documented response is to re-fetch, re-select and repeat — see #with_key_rotation. 21_470
Instance Method Summary collapse
-
#all ⇒ Array<Certificate>
The cached list, fetching it if stale.
-
#for_usage(kind, at: nil) ⇒ Certificate
Applies the documented selection rule: filter by
usage, require validity at the moment of use, and where several qualify prefer the latestvalidFrom. -
#initialize(connection, ttl: DEFAULT_TTL, clock: -> { Time.now }) ⇒ PublicKeys
constructor
A new instance of PublicKeys.
-
#refresh! ⇒ Array<Certificate>
Discards the cache and fetches again.
-
#symmetric_key_encryption(at: nil) ⇒ Object
The key that wraps a session's AES key (§10.1).
-
#token_encryption(at: nil) ⇒ Object
The key that wraps the KSeF token during authentication (§4.5).
-
#with_key_rotation ⇒ Object
Runs an operation, and on the one error that says "your key is stale" re-fetches the list and runs it again.
Constructor Details
#initialize(connection, ttl: DEFAULT_TTL, clock: -> { Time.now }) ⇒ PublicKeys
Returns a new instance of PublicKeys.
41 42 43 44 45 46 47 48 |
# File 'lib/ksef/crypto/public_keys.rb', line 41 def initialize(connection, ttl: DEFAULT_TTL, clock: -> { Time.now }) @connection = connection @ttl = ttl @clock = clock @mutex = Mutex.new @certificates = nil @fetched_at = nil end |
Instance Method Details
#all ⇒ Array<Certificate>
Returns the cached list, fetching it if stale.
51 52 53 |
# File 'lib/ksef/crypto/public_keys.rb', line 51 def all @mutex.synchronize { cached || load! } end |
#for_usage(kind, at: nil) ⇒ Certificate
Applies the documented selection rule: filter by usage, require validity at
the moment of use, and where several qualify prefer the latest validFrom.
Not a judgement call — §10.2 states it, which matters because during a planned rotation overlap two certificates are legitimately valid for the same usage and picking the older one wastes the overlap the Ministry provided.
75 76 77 78 79 80 |
# File 'lib/ksef/crypto/public_keys.rb', line 75 def for_usage(kind, at: nil) validate_usage!(kind) moment = at || now usable = all.select { |certificate| certificate.usable_for?(kind) && certificate.valid_at?(moment) } usable.max_by(&:valid_from) || raise(CryptoError, nothing_valid(kind, moment)) end |
#refresh! ⇒ Array<Certificate>
Discards the cache and fetches again. The recovery path of §10.2 after a key rotation, and the only way to see a newly published certificate before the TTL lapses.
60 61 62 |
# File 'lib/ksef/crypto/public_keys.rb', line 60 def refresh! @mutex.synchronize { load! } end |
#symmetric_key_encryption(at: nil) ⇒ Object
The key that wraps a session's AES key (§10.1).
86 |
# File 'lib/ksef/crypto/public_keys.rb', line 86 def symmetric_key_encryption(at: nil) = for_usage(Certificate::SYMMETRIC_KEY_ENCRYPTION, at: at) |
#token_encryption(at: nil) ⇒ Object
The key that wraps the KSeF token during authentication (§4.5).
83 |
# File 'lib/ksef/crypto/public_keys.rb', line 83 def token_encryption(at: nil) = for_usage(Certificate::KSEF_TOKEN_ENCRYPTION, at: at) |
#with_key_rotation ⇒ Object
Runs an operation, and on the one error that says "your key is stale" re-fetches the list and runs it again.
This does not contradict the never-auto-retry-a-POST rule (DESIGN.md §6.7). A
21470 is the API declining the request outright, so nothing happened server-side
and there is no duplicate to create; and this is remediation — the second attempt
carries a different key identifier — rather than a blind replay. §10.2 prescribes
exactly this sequence.
The block must therefore re-select the certificate itself, so pass the whole operation rather than a pre-built request:
keys.with_key_rotation do
session.open(encryption: encryptor.encryption_info(keys.symmetric_key_encryption))
end
105 106 107 108 109 110 111 112 |
# File 'lib/ksef/crypto/public_keys.rb', line 105 def with_key_rotation yield rescue ApiError => e raise unless e.code == UNKNOWN_KEY_CODE refresh! yield end |