A single X509 key is generated per distribution platform (Rubygems.org, Gemfury, etc). This key is used to sign gem author requests.
A gem author may generate a certificate and request that the platform sign it. Alice generates her x509 keypair with her email address encoded as the x509 name field, stashes the private key somewhere safe, and submits the pubkey to the signing system.
The signing system consists of two parts:
- [Machine A] A web UI (or email inbox) responsible for accepting public keys and sending emails
- [Machine B] A signing machine with a shared data store (shared NFS mount, redis store, whatever - it must simply be a data store to act as a dead drop)
The UI accepts pubkeys, ensures their validity, parses the certificate for the name field, and sends a verification email to the email specified in the name field. The email contains a link with a cryptographic signature (something like an HMAC of the pubkey). The email owner clicks this link (or replies to the email) which causes Machine A to validate the response and put the affiliated pubkey into the dead-drop inbox.
Machine B is monitoring the inbox for pubkeys. Once a key is received, it is signed, and placed in the dead-drop outbox.
Machine A monitors the outbox for signed keys. It again parses the key for the name field again, encrypts the signed key with itself, and emails it to the name field in the record.
Alice retrieves the key from her email inbox, decrypts it with her private key, and then may use it to sign her gems.
This system could have an exceptionally small attack surface, consisting of only a minimal mailserver (A) and a local-only daemon (B) which operate on shared storage (on either A or B, or on a third server, C).
A separate server ["Chain of trust history server"] maintains and validates cert chain history for all gems on Rubygems.org, but which is queryable by Rubygems-bin, allowing Rubygems-bin to obtain the last known and verified certificate chain for a given gem when installing in the event that no local history is known.
It must be separate from the Rubygems.org platform in order to avoid allowing a compromise of Rubygems.org to be pivoted into a compromise of the certificate history system, allowing an attacker to upload fraudulent certificates.
This system would naturally serve as an automated IDS, as well, and could raise an alarm if it ever discovered that Rubygems.org had accepted a gem without a valid certificate chain, indicating a breach of the system's certificate verification mechanisms.
The gem is signed with something like:
s.signing_key = "alice-private_cert.pem"
s.cert_chain = ['rubygems-public_cert.pem', 'alice-public_cert.pem']
Alice may then upload her gem to Rubygems.org. Upon receipt of the gem, Rubygems.org ensures that the gem has been signed with a cert chain terminating in a certificate that it knows about and trusts. Additionally, it will ensure that the gem is signed with a certificate containing an email that matches the email on the account of the system.
Rubygems(-bin) will maintain a local history of certificate chains for a gem. If a certificate is removed, then it will refuse to install the gem, suggest review, and require a user override to proceed. Rubygems.org will additionally maintain this certificate chain, and refuse to accept a gem that does not include the owning account's email as a part of the chain of trust. This ensures:
- If an individual Rubygems.org account is compromised (but not the legitimate owner's private key), then a malicious entity cannot upload a modified gem into the account.
- If an individual Rubygems.org account is comprimised, and the attacker has been able to forge a key with the account's email, then the attacker can upload new gems into the account, but cannot publish new versions of existing gems, as they will fail to validate the chain of trust history.
- If Rubygems as a whole is compromised, then the attacker may be able to upload a malicious gem. However, Rubygems-bin will refuse to install any newer version of it.
Rubygems will allow certificates to be added to the certificate chain, so long as they are signed by a non-root certificate in the chain. This permits for transfer of project ownership and multiple signing keys. For example:
Alice starts a project, Foobar, signs it with her key. The chain now looks like:
[rubygems, alice]
Alice then later abandons the project, and Charlie takes over as maintainer with Alice's blessing. Alice signs Charlie's public key, so the chain now looks like:
[rubygems, alice, charlie]
As Alice signed Charlie's key, she is still part of the chain of trust, the system permits installation, with the implicit understanding that Alice has blessed Charlie's key.
If Dave, a malicious actor, managed to wrest control of the project, he would be able to sign the gem, but its trust chain would look like:
[rubygems, dave]
Thus, both Rubygems.org and Rubygems-bin would reject the gem based on the gem's known certificate history, and Alice's exclusion from the certificate chain.
Bob, a Ruby developer, wants to use Alice's gem. Bob would install the Rubygems.org public cert as a trusted certificate:
gem cert --add rubygems-public_cert.pem
Bob may then download and install Alice's gem, and Rubygems(-bin)'s HighSecurity policy will validate and accept the gem, and permit it to install.
Before fetching a gem, Rubygems would need to fetch any certificate revocation lists. It would then check the trusted certificate list for revocations, and remove any that appear on the list. This is the primary mechanism in which a compromised CA key would be removed. Users would be required to manually install the new key in this event.
This necessitates that the Rubygems public key must be published in a location that is not connected to the CA, as a compromise of the CA could allow an attacker to revoke the otherwise-legitimate root key and publish his own for consumption.
Each time Rubygems runs a network operation, it should
- Check if the revocation list has changed since the last time it validated certificates for known gems.
- If the list has changed, validate the certificate chains for all installed gems. Prompt to remove any with invalid certificates.
- If step 2 was run, write a hash of the revocation list and the list of gems that passed muster.
- Remove any entries from the local chain of trust history that contain revoked certificates.
- Check for a new revocation list
- Run step 2 if the revocation list has changed.
This allows for certificate verifications and revocations for multiple gem installs (RVM gemsets, bundler local installs) in a given system.
- Installation of a malicious certificate as a trusted root certificate on a local machine would result in signatures becoming unreliable. However, given that this would require some level of ownership of the machine, it would likely be a small problem in such an event.
- Compromise of Rubygems.org's distribution platform may result in the upload of malicious gems. Such gems would be distributed to gem installers, which would then reject the gems due to either a local failed chain of trust, or a failed chain of trust from the chain history server.
- Compromise of the chain history server would not be exploitable to install malicious software, as the attacker must also have control of the distribution platform. MITM attacks would be viable, but if you can MITM Rubygems.org, you can MITM chain history server queries.
- Compromise of the chain history server AND Rubygems.org would allow for attackers to upload compromised gems to Rubygems.org and distribute them to pristine installs. Upgrades would still fail due to the local chain of trust history.
- Compromise of the Rubygems' pubkey publication platform could result in an attacker publishing his own public key, which would affect people installing the certificate for the first time. However, legitimate gems from Rubygems.org would fail to install as they were not signed with the attacker's keypair.
- Compromise of the pubkey platform AND the Rubygems.org platform would result in failure to install due to local or queried chain of trust histories.
- Compromise of the CA's "Machine A" would result in people being able to obtain signed keys for emails without validation. It would not expose the private key for Machine B. This would permit uploading of new gems to a compromised user account, but new versions of existing gems would fail to upload, as the key provided would not be a part of the gem's existing chain of trust history.
- Compromise of the CA's "Machine B" would result in disclosure of the private key, requiring that the root key be revoked and reissued. This would invalidate all current gem signatures. Illicit replacement of the private key on CA's "Machine B" would result in people being issued certificates that would fail to upload to Rubygems.org, due to failure to validate the cert chain against the Rubygems.org private key.
- Compromise of the CA and Rubygems.org would result in pristine installs being served malicious software. Upgrades would still fail due to local chain of trust history.
- An author's stolen private key may be used to fraudulently sign requests. This may be defended against by following proper key protection measures and password-protecting the key.
- Most raised MITM attacks can be avoided by performing Rubygems.org and chain history queries via SSL.