@@ -7,8 +7,16 @@ package azkv // import "github.com/getsops/sops/v3/azkv"
77
88import (
99 "context"
10+ "crypto/rand"
11+ "crypto/rsa"
12+ "crypto/sha256"
13+ "crypto/x509"
1014 "encoding/base64"
15+ "encoding/json"
16+ "encoding/pem"
1117 "fmt"
18+ "math/big"
19+ "os"
1220 "regexp"
1321 "strings"
1422 "time"
@@ -54,6 +62,9 @@ type MasterKey struct {
5462 // CreationDate of the MasterKey, used to determine if the EncryptedKey
5563 // needs rotation.
5664 CreationDate time.Time
65+ // PublicKey contains an optional locally provided public key used for
66+ // offline encryption. Decryption still uses Azure Key Vault.
67+ PublicKey []byte
5768
5869 // tokenCredential contains the azcore.TokenCredential used by the Azure
5970 // client. It can be injected by a (local) keyservice.KeyServiceServer
@@ -64,6 +75,18 @@ type MasterKey struct {
6475 clientOptions * azkeys.ClientOptions
6576}
6677
78+ type jsonWebKeyEnvelope struct {
79+ Key * jsonWebKey `json:"key"`
80+ }
81+
82+ type jsonWebKey struct {
83+ Kid string `json:"kid"`
84+ Kty string `json:"kty"`
85+ N string `json:"n"`
86+ E string `json:"e"`
87+ X5c []string `json:"x5c"`
88+ }
89+
6790// newMasterKey creates a new MasterKey from a URL, key name and version,
6891// setting the creation date to the current date.
6992func newMasterKey (vaultURL string , keyName string , keyVersion string ) * MasterKey {
@@ -91,6 +114,30 @@ func NewMasterKeyWithOptionalVersion(vaultURL string, keyName string, keyVersion
91114 return key , nil
92115}
93116
117+ // NewMasterKeyWithPublicKey creates a new MasterKey that encrypts data keys
118+ // offline with the provided public key and decrypts them through Azure Key Vault.
119+ func NewMasterKeyWithPublicKey (vaultURL string , keyName string , keyVersion string , publicKey []byte ) (* MasterKey , error ) {
120+ key := newMasterKey (vaultURL , keyName , keyVersion )
121+ key .PublicKey = append ([]byte (nil ), publicKey ... )
122+
123+ if key .Version == "" {
124+ return nil , fmt .Errorf ("azure key vault offline encryption requires a key version" )
125+ }
126+ if _ , err := parsePublicKey (key .PublicKey ); err != nil {
127+ return nil , err
128+ }
129+ return key , nil
130+ }
131+
132+ // NewMasterKeyWithPublicKeyFile creates a new MasterKey from a local public key file.
133+ func NewMasterKeyWithPublicKeyFile (vaultURL string , keyName string , keyVersion string , publicKeyPath string ) (* MasterKey , error ) {
134+ publicKey , err := os .ReadFile (publicKeyPath )
135+ if err != nil {
136+ return nil , fmt .Errorf ("failed to read Azure Key Vault public key file %q: %w" , publicKeyPath , err )
137+ }
138+ return NewMasterKeyWithPublicKey (vaultURL , keyName , keyVersion , publicKey )
139+ }
140+
94141// NewMasterKeyFromURL takes an Azure Key Vault key URL, and returns a new
95142// MasterKey. The URL format is {vaultUrl}/keys/{keyName}/{keyVersion}.
96143func NewMasterKeyFromURL (url string ) (* MasterKey , error ) {
@@ -171,7 +218,7 @@ func (key *MasterKey) Encrypt(dataKey []byte) error {
171218}
172219
173220func (key * MasterKey ) ensureKeyHasVersion (ctx context.Context ) error {
174- if ( key .Version != "" ) {
221+ if key .Version != "" {
175222 // Nothing to do
176223 return nil
177224 }
@@ -203,6 +250,10 @@ func (key *MasterKey) ensureKeyHasVersion(ctx context.Context) error {
203250// EncryptContext takes a SOPS data key, encrypts it with Azure Key Vault, and stores
204251// the result in the EncryptedKey field.
205252func (key * MasterKey ) EncryptContext (ctx context.Context , dataKey []byte ) error {
253+ if len (key .PublicKey ) > 0 {
254+ return key .encryptOffline (dataKey )
255+ }
256+
206257 token , err := key .getTokenCredential ()
207258 if err != nil {
208259 log .WithFields (logrus.Fields {"key" : key .Name , "version" : key .Version }).Info ("Encryption failed" )
@@ -230,6 +281,23 @@ func (key *MasterKey) EncryptContext(ctx context.Context, dataKey []byte) error
230281 return nil
231282}
232283
284+ func (key * MasterKey ) encryptOffline (dataKey []byte ) error {
285+ publicKey , err := parsePublicKey (key .PublicKey )
286+ if err != nil {
287+ log .WithFields (logrus.Fields {"key" : key .Name , "version" : key .Version }).Info ("Encryption failed" )
288+ return err
289+ }
290+
291+ encryptedKey , err := rsa .EncryptOAEP (sha256 .New (), rand .Reader , publicKey , dataKey , nil )
292+ if err != nil {
293+ log .WithFields (logrus.Fields {"key" : key .Name , "version" : key .Version }).Info ("Encryption failed" )
294+ return fmt .Errorf ("failed to encrypt sops data key locally with Azure Key Vault public key '%s': %w" , key .ToString (), err )
295+ }
296+ key .SetEncryptedDataKey ([]byte (base64 .RawURLEncoding .EncodeToString (encryptedKey )))
297+ log .WithFields (logrus.Fields {"key" : key .Name , "version" : key .Version }).Info ("Offline encryption succeeded" )
298+ return nil
299+ }
300+
233301// EncryptedDataKey returns the encrypted data key this master key holds.
234302func (key * MasterKey ) EncryptedDataKey () []byte {
235303 return []byte (key .EncryptedKey )
@@ -306,6 +374,9 @@ func (key MasterKey) ToMap() map[string]interface{} {
306374 out ["vaultUrl" ] = key .VaultURL
307375 out ["key" ] = key .Name
308376 out ["version" ] = key .Version
377+ if len (key .PublicKey ) > 0 {
378+ out ["public_key" ] = base64 .StdEncoding .EncodeToString (key .PublicKey )
379+ }
309380 out ["created_at" ] = key .CreationDate .UTC ().Format (time .RFC3339 )
310381 out ["enc" ] = key .EncryptedKey
311382 return out
@@ -324,3 +395,85 @@ func (key *MasterKey) getTokenCredential() (azcore.TokenCredential, error) {
324395 }
325396 return key .tokenCredential , nil
326397}
398+
399+ func parsePublicKey (raw []byte ) (* rsa.PublicKey , error ) {
400+ if jwk , err := parseJSONWebKey (raw ); err == nil {
401+ return jwk , nil
402+ }
403+ if pemKey , err := parsePEMPublicKey (raw ); err == nil {
404+ return pemKey , nil
405+ }
406+ return nil , fmt .Errorf ("failed to parse Azure Key Vault public key: expected RSA JWK or PEM" )
407+ }
408+
409+ func parseJSONWebKey (raw []byte ) (* rsa.PublicKey , error ) {
410+ var key jsonWebKey
411+ if err := json .Unmarshal (raw , & key ); err != nil {
412+ var envelope jsonWebKeyEnvelope
413+ if err := json .Unmarshal (raw , & envelope ); err != nil || envelope .Key == nil {
414+ return nil , fmt .Errorf ("invalid JSON Web Key" )
415+ }
416+ key = * envelope .Key
417+ }
418+ if key .N == "" || key .E == "" {
419+ return nil , fmt .Errorf ("json web key is missing modulus or exponent" )
420+ }
421+ if key .Kty != "" && key .Kty != "RSA" && key .Kty != "RSA-HSM" {
422+ return nil , fmt .Errorf ("unsupported Azure Key Vault key type %q" , key .Kty )
423+ }
424+
425+ modulusBytes , err := base64 .RawURLEncoding .DecodeString (key .N )
426+ if err != nil {
427+ return nil , fmt .Errorf ("failed to decode JSON Web Key modulus: %w" , err )
428+ }
429+ exponentBytes , err := base64 .RawURLEncoding .DecodeString (key .E )
430+ if err != nil {
431+ return nil , fmt .Errorf ("failed to decode JSON Web Key exponent: %w" , err )
432+ }
433+ exponent := 0
434+ for _ , b := range exponentBytes {
435+ exponent = exponent << 8 + int (b )
436+ }
437+ if exponent == 0 {
438+ return nil , fmt .Errorf ("json web key exponent is invalid" )
439+ }
440+
441+ return & rsa.PublicKey {
442+ N : new (big.Int ).SetBytes (modulusBytes ),
443+ E : exponent ,
444+ }, nil
445+ }
446+
447+ func parsePEMPublicKey (raw []byte ) (* rsa.PublicKey , error ) {
448+ block , _ := pem .Decode (raw )
449+ if block == nil {
450+ return nil , fmt .Errorf ("no PEM block found" )
451+ }
452+
453+ switch block .Type {
454+ case "PUBLIC KEY" :
455+ publicKey , err := x509 .ParsePKIXPublicKey (block .Bytes )
456+ if err != nil {
457+ return nil , err
458+ }
459+ rsaKey , ok := publicKey .(* rsa.PublicKey )
460+ if ! ok {
461+ return nil , fmt .Errorf ("PEM public key is not RSA" )
462+ }
463+ return rsaKey , nil
464+ case "RSA PUBLIC KEY" :
465+ return x509 .ParsePKCS1PublicKey (block .Bytes )
466+ case "CERTIFICATE" :
467+ cert , err := x509 .ParseCertificate (block .Bytes )
468+ if err != nil {
469+ return nil , err
470+ }
471+ rsaKey , ok := cert .PublicKey .(* rsa.PublicKey )
472+ if ! ok {
473+ return nil , fmt .Errorf ("certificate public key is not RSA" )
474+ }
475+ return rsaKey , nil
476+ default :
477+ return nil , fmt .Errorf ("unsupported PEM block type %q" , block .Type )
478+ }
479+ }
0 commit comments