forked from sigstore/fulcio
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add file backed certificate authority (sigstore#280)
Loads private key and certificate from disk. Optionally watches for file changes and loads updated key pair. Signed-off-by: Nathan Smith <nathan@nfsmith.ca>
- Loading branch information
Showing
20 changed files
with
759 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
// Copyright 2021 The Sigstore Authors. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
|
||
package fileca | ||
|
||
import ( | ||
"context" | ||
"crypto" | ||
"crypto/rand" | ||
"crypto/x509" | ||
"encoding/pem" | ||
"sync" | ||
|
||
"github.com/fsnotify/fsnotify" | ||
"github.com/sigstore/fulcio/pkg/ca" | ||
"github.com/sigstore/fulcio/pkg/ca/x509ca" | ||
"github.com/sigstore/fulcio/pkg/challenges" | ||
) | ||
|
||
type fileCA struct { | ||
sync.RWMutex | ||
|
||
cert *x509.Certificate | ||
key crypto.Signer | ||
} | ||
|
||
// NewFileCA returns a file backed certificate authority. Expects paths to a | ||
// certificate and key that are PEM encoded. The key must be encrypted | ||
// according to RFC 1423 | ||
func NewFileCA(certPath, keyPath, keyPass string, watch bool) (ca.CertificateAuthority, error) { | ||
var fca fileCA | ||
|
||
var err error | ||
fca.cert, fca.key, err = loadKeyPair(certPath, keyPath, keyPass) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if watch { | ||
watcher, err := fsnotify.NewWatcher() | ||
if err != nil { | ||
return nil, err | ||
} | ||
err = watcher.Add(certPath) | ||
if err != nil { | ||
return nil, err | ||
} | ||
err = watcher.Add(keyPath) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
go ioWatch(certPath, keyPath, keyPass, watcher, fca.updateX509KeyPair) | ||
} | ||
|
||
return &fca, err | ||
} | ||
|
||
func (fca *fileCA) updateX509KeyPair(cert *x509.Certificate, key crypto.Signer) { | ||
fca.Lock() | ||
defer fca.Unlock() | ||
|
||
// NB: We use the RWLock to unsure a reading thread can't get a mismatching | ||
// cert / key pair by reading the attributes halfway through the update | ||
// below. | ||
fca.cert = cert | ||
fca.key = key | ||
} | ||
|
||
func (fca *fileCA) getX509KeyPair() (*x509.Certificate, crypto.Signer) { | ||
fca.RLock() | ||
defer fca.RUnlock() | ||
return fca.cert, fca.key | ||
} | ||
|
||
// CreateCertificate issues code signing certificates | ||
func (fca *fileCA) CreateCertificate(_ context.Context, subject *challenges.ChallengeResult) (*ca.CodeSigningCertificate, error) { | ||
cert, err := x509ca.MakeX509(subject) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
rootCA, privateKey := fca.getX509KeyPair() | ||
|
||
finalCertBytes, err := x509.CreateCertificate(rand.Reader, cert, rootCA, subject.PublicKey, privateKey) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return ca.CreateCSCFromDER(subject, finalCertBytes, nil) | ||
} | ||
|
||
func (fca *fileCA) Root(ctx context.Context) ([]byte, error) { | ||
return pem.EncodeToMemory(&pem.Block{ | ||
Type: "CERTIFICATE", | ||
Bytes: fca.cert.Raw, | ||
}), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
// Copyright 2021 The Sigstore Authors. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
|
||
package fileca | ||
|
||
import ( | ||
"crypto/ecdsa" | ||
"crypto/ed25519" | ||
"testing" | ||
) | ||
|
||
const testKeyPass = `password123` | ||
|
||
func TestNewFileCA(t *testing.T) { | ||
_, err := NewFileCA( | ||
`testdata/ed25519-cert.pem`, | ||
`testdata/ed25519-key.pem`, | ||
testKeyPass, | ||
false, | ||
) | ||
if err != nil { | ||
t.Error(`Failed to load file CA from disk`) | ||
} | ||
} | ||
|
||
func TestCertUpdate(t *testing.T) { | ||
oldCert := `testdata/ed25519-cert.pem` | ||
oldKey := `testdata/ed25519-key.pem` | ||
newCert := `testdata/ecdsa-cert.pem` | ||
newKey := `testdata/ecdsa-key.pem` | ||
watch := false | ||
|
||
ca, err := NewFileCA( | ||
oldCert, | ||
oldKey, | ||
testKeyPass, | ||
watch, | ||
) | ||
if err != nil { | ||
t.Fatal(`Failed to load file CA from disk`) | ||
} | ||
|
||
fca, ok := ca.(*fileCA) | ||
if !ok { | ||
t.Fatal(`Bad CA type`) | ||
} | ||
|
||
_, key := fca.getX509KeyPair() | ||
if _, ok = key.(ed25519.PrivateKey); !ok { | ||
t.Error(`first key should have been an ed25519 key`) | ||
} | ||
|
||
cert, key, err := loadKeyPair(newCert, newKey, testKeyPass) | ||
if err != nil { | ||
t.Fatal(`Failed to load new keypair`) | ||
} | ||
|
||
fca.updateX509KeyPair(cert, key) | ||
_, key = fca.getX509KeyPair() | ||
|
||
if _, ok = key.(*ecdsa.PrivateKey); !ok { | ||
t.Fatal(`file CA should have been updated with ecdsa key`) | ||
} | ||
} |
Oops, something went wrong.