-
Notifications
You must be signed in to change notification settings - Fork 2
rfc: proposal for standard private key handling #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| # RFC: Standardizing Private Key Material Handling | ||
|
|
||
| ## Summary | ||
|
|
||
| This RFC proposes standardizing how we handle private key material across our services, adopting PEM format as the single standard for private key storage and configuration. | ||
|
|
||
| ## Problem Statement | ||
|
|
||
| Our services currently accept private keys in multiple formats: | ||
|
|
||
| - PEM files (with inconsistent encryption practices). | ||
| - JSON files (non-standard, service-specific schemas). | ||
| - Base64 encoded strings (raw key material, no metadata). | ||
| - Multibase base64 padded strings (uncommon in standard tooling). | ||
|
|
||
| This creates: | ||
|
|
||
| - **Security inconsistency**: Different formats have varying security properties. | ||
| - **Implementation complexity**: Each service implements multiple parsers. | ||
| - **Cognitive overhead**: Developers must remember which format works where. | ||
| - **Error-prone deployments**: Format mismatches cause configuration failures. | ||
|
|
||
| ## Proposed Solution: PEM Standard | ||
|
|
||
| **Format**: PEM-encoded private keys using PKCS#8 format | ||
|
|
||
| ``` | ||
| -----BEGIN PRIVATE KEY----- | ||
| MC4CAQA... | ||
| -----END PRIVATE KEY----- | ||
| ``` | ||
|
|
||
| ### Why PEM? | ||
|
|
||
| 1. **Universal tooling support**: OpenSSL, ssh-keygen, and all major crypto libraries support PEM natively. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but typically we want to display the DID at the same time, so we have to dip back into our own tooling anyways.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| 2. **Self-documenting format**: Clear headers (`-----BEGIN PRIVATE KEY-----`) identify content type. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤷 the multibase encoded string is also self describing - the decoded bytes are also prefixed with the alogorithm. PEM has the advantage of being human readable to some extent.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My main concerns with multibase for private key material center around tooling, file conventions, and security.
At the end of the day, specifically for the storage node, I'd prefer to maintain key material in a file, rather than a string passed over the CLI or environment variable. |
||
| 3. **Algorithm agnostic**: Supports our current Ed25519 keys and future algorithms (RSA, ECDSA, etc.). | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, same for existing format. |
||
| 4. **Built-in security**: Native support for passphrase encryption. | ||
| 5. **Human readable**: Base64 encoded with clear delimiters for debugging. | ||
| 6. **Industry standard**: Developers already understand `.pem` files. | ||
|
|
||
| ### Usage Examples | ||
|
|
||
| **Generating Ed25519 keys**: | ||
|
|
||
| ```bash | ||
| # Generate Ed25519 private key | ||
| openssl genpkey -algorithm ed25519 -out service-key.pem | ||
|
|
||
| # View key details | ||
| openssl pkey -in service-key.pem -text -noout | ||
|
|
||
| # Extract public key | ||
| openssl pkey -in service-key.pem -pubout -out service-pubkey.pem | ||
| ``` | ||
|
|
||
| **Securing keys**: | ||
|
|
||
| ```bash | ||
| # Set proper permissions | ||
| chmod 600 service-key.pem | ||
|
|
||
| # Encrypt for storage | ||
| openssl pkey -in service-key.pem -out service-key-encrypted.pem -aes256 | ||
|
|
||
| # Decrypt when needed | ||
| openssl pkey -in service-key-encrypted.pem -out service-key.pem | ||
| ``` | ||
|
|
||
| **Future algorithm support** (no code changes needed): | ||
|
|
||
| ```bash | ||
| # RSA | ||
| openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:4096 -out rsa-key.pem | ||
|
|
||
| # ECDSA | ||
| openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-256 -out ecdsa-key.pem | ||
| ``` | ||
|
|
||
| ## Security Requirements | ||
|
|
||
| 1. **File permissions**: Private key files MUST have 600 permissions. | ||
| 2. **No environment variables**: Private keys MUST NOT be passed via environment variables. | ||
| 3. **Encryption at rest**: Keys SHOULD be encrypted when stored. | ||
| 4. **Memory handling**: Keys SHOULD be loaded once at startup, not repeatedly read. | ||
| 5. **Secure deletion**: Key material SHOULD be securely overwritten when removed from memory. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have historically used AWS SSM for deployed services and would continue to do so regardless of encoding (I understand this is not the same situation for storage nodes). |
||
|
|
||
| ## Implementation Guidelines | ||
|
|
||
| ### Service Configuration | ||
|
|
||
| ```go | ||
| type Config struct { | ||
| PrivateKeyPath string `flag:"private-key" env:"-" description:"Path to PEM-encoded private key"` | ||
| } | ||
|
|
||
| func loadPrivateKey(path string) (crypto.PrivateKey, error) { | ||
| // Check file permissions | ||
| info, err := os.Stat(path) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("accessing private key file: %w", err) | ||
| } | ||
| if info.Mode().Perm() != 0600 { | ||
| return nil, fmt.Errorf("private key file must have 600 permissions, has %v", info.Mode().Perm()) | ||
| } | ||
|
|
||
| pemData, err := os.ReadFile(path) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("reading private key: %w", err) | ||
| } | ||
|
|
||
| block, _ := pem.Decode(pemData) | ||
| if block == nil { | ||
| return nil, errors.New("failed to parse PEM block") | ||
| } | ||
|
|
||
| // Require unencrypted keys for services | ||
| // NB: depending on the context this method is called in, we may prompt for passphrase. | ||
| if x509.IsEncryptedPEMBlock(block) { | ||
| return nil, errors.New("encrypted private keys are not supported; " + | ||
| "use file permissions (600) for security instead") | ||
| } | ||
|
|
||
| // Parse unencrypted PKCS#8 private key | ||
| privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) | ||
| if err != nil { | ||
| // Fallback for legacy formats | ||
| if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { | ||
| privateKey = key | ||
| } else if key, err := x509.ParseECPrivateKey(block.Bytes); err == nil { | ||
| privateKey = key | ||
| } else { | ||
| return nil, fmt.Errorf("parsing private key: %w", err) | ||
| } | ||
| } | ||
|
|
||
| // Validate and return supported key types | ||
| // NB: at present we only support ed25519 | ||
| switch key := privateKey.(type) { | ||
| case ed25519.PrivateKey: | ||
| return key, nil | ||
| default: | ||
| return nil, fmt.Errorf("unsupported private key type: %T", privateKey) | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Standard File Locations | ||
|
|
||
| - Development: `./keys/service-key.pem` | ||
| - Production: `/etc/service/keys/service-key.pem` | ||
| - Container: `/run/secrets/service-key` | ||
|
|
||
| ## Benefits | ||
|
|
||
| - **Enhanced security**: Consistent security model across all services. | ||
| - **Reduced complexity**: Single format reduces code and documentation. | ||
| - **Better tooling**: Leverage decades of OpenSSL development. | ||
| - **Future proof**: Algorithm changes require no format changes. | ||
| - **Developer friendly**: Well-understood format with extensive documentation. | ||
|
|
||
| ## DID Derivation from Private Keys | ||
|
|
||
| Users often need to derive a DID (Decentralized Identifier) from the private key for identity purposes. This section briefly covers how to extract the DID from a PEM-encoded private key. | ||
|
|
||
| ### What is a DID and How do we Encode ours? | ||
|
|
||
| A DID is a decentralized identifier that corresponds to your public key. In our system, Ed25519 keys produce DIDs in the format: `did:key:...` where the suffix is a Base58BTC-encoded representation of the public key with multicodec prefixes. | ||
|
|
||
| ### Deriving DID from PEM | ||
|
|
||
| At present, there does not exist any standard tooling to derive a DID used by our system from a PEM File containing a private key, or public key for that matter. This necessitates the creation of a custom CLI tool which allows users to derive a DID string from their public key. | ||
|
Comment on lines
+162
to
+172
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @alanshaw assuming we align on PEM as our mechanism for handling key material, curious to hear your thoughts on these bits. |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok a few observations:
So there was only really the 1 format prior - for which some of the reasons below don't apply.
I'm not trying to oppose the proposal here but I think if this was phrased more as "switch from multibase base64 padded private keys to PEM" then I think it would be more accurate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're absolutely right on all points - thank you for the clarification.
You're correct - looking at my shell history, we've been using multibase base64 padded strings, not plain base64.
Yes, I did introduce PEM to the storage node. The current state is:
storage id genreturns JSON with a DID and multibase-base64-padded private key.--type=pemoption for PEM output (needed for Curio's API authentication).storage id parseto convert between formats, and to extract DIDs from PEM files.The PEM format was specifically introduced to work with Curio's API, which requires PEM-formatted public keys for authentication.
Yeah, the accurate framing is probably: "Switch from multibase base64 padded private keys to PEM". JSON is only used as an output wrapper in some cases, not as a key format itself.
It's worth noting that at the end of this doc the need for custom tooling to extract DIDs from PEM files is outlined. Curious what your thoughts are on this.