extension serialization (#1554)

This commit is contained in:
Bryan White 2019-04-03 17:03:53 +02:00 committed by GitHub
parent e2a43c3fb6
commit fe476fdcf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 623 additions and 461 deletions

View File

@ -111,7 +111,7 @@ func saveIdentityTar(path string, key crypto.PrivateKey, id storj.NodeID) error
return err
}
caCert, err := peertls.NewCert(key, nil, ct, nil)
caCert, err := peertls.NewSelfSignedCert(key, ct)
if err != nil {
return err
}

View File

@ -139,7 +139,7 @@ func cmdRevokeCA(cmd *cobra.Command, args []string) (err error) {
return err
}
if err := extensions.AddRevocationExt(ca.Key, ca.Cert, ca.Cert); err != nil {
if err := ca.Revoke(); err != nil {
return err
}
@ -211,5 +211,5 @@ func cmdCAExtensions(cmd *cobra.Command, args []string) (err error) {
return err
}
return printExtensions(ca.Cert.Raw, ca.Cert.ExtraExtensions)
return printExtensions(ca.Cert.Raw, ca.Cert.Extensions)
}

View File

@ -11,7 +11,6 @@ import (
"storj.io/storj/pkg/cfgstruct"
"storj.io/storj/pkg/identity"
"storj.io/storj/pkg/peertls/extensions"
)
var (
@ -57,7 +56,7 @@ var (
revokeLeafCfg struct {
CA identity.FullCAConfig
Identity identity.PeerConfig
Identity identity.Config
// TODO: add "broadcast" option to send revocation to network nodes
}
)
@ -102,7 +101,7 @@ func cmdLeafExtensions(cmd *cobra.Command, args []string) (err error) {
return err
}
return printExtensions(ident.Leaf.Raw, ident.Leaf.ExtraExtensions)
return printExtensions(ident.Leaf.Raw, ident.Leaf.Extensions)
}
func cmdRevokeLeaf(cmd *cobra.Command, args []string) (err error) {
@ -115,24 +114,17 @@ func cmdRevokeLeaf(cmd *cobra.Command, args []string) (err error) {
return err
}
updatedIdent, err := ca.NewIdentity()
if err != nil {
manageableIdent := identity.NewManageableFullIdentity(originalIdent, ca)
if err := manageableIdent.Revoke(); err != nil {
return err
}
if err := extensions.AddRevocationExt(ca.Key, originalIdent.Leaf, updatedIdent.Leaf); err != nil {
return err
}
// NB: backup original cert
// NB: backup original cert and key.
if err := revokeLeafCfg.Identity.SaveBackup(originalIdent); err != nil {
return err
}
updateCfg := identity.Config{
CertPath: revokeLeafCfg.Identity.CertPath,
}
if err := updateCfg.Save(updatedIdent); err != nil {
if err := revokeLeafCfg.Identity.Save(manageableIdent.FullIdentity); err != nil {
return err
}
return nil

View File

@ -53,12 +53,7 @@ func cmdRevocations(cmd *cobra.Command, args []string) error {
revErrs := new(errs.Group)
for _, rev := range revs {
// TODO: figure out why this doesn't base64 encode []byte fields!
//revJSON, err := json.MarshalIndent(rev.CertHash, "", "\t")
//if err != nil {
// revErrs.Add(err)
//}
fmt.Printf("certificate hash: %s\n", base64.StdEncoding.EncodeToString(rev.CertHash))
fmt.Printf("certificate public key hash: %s\n", base64.StdEncoding.EncodeToString(rev.KeyHash))
fmt.Printf("\timestamp: %s\n", time.Unix(rev.Timestamp, 0).String())
fmt.Printf("\tsignature: %s\n", base64.StdEncoding.EncodeToString(rev.Signature))
}

View File

@ -33,3 +33,31 @@ func NewTestCA(ctx context.Context) (*identity.FullCertificateAuthority, error)
Concurrency: 1,
})
}
// NewTestManageablePeerIdentity returns a new manageable peer identity for use in tests.
func NewTestManageablePeerIdentity(ctx context.Context) (*identity.ManageablePeerIdentity, error) {
ca, err := NewTestCA(ctx)
if err != nil {
return nil, err
}
ident, err := ca.NewIdentity()
if err != nil {
return nil, err
}
return identity.NewManageablePeerIdentity(ident.PeerIdentity(), ca), nil
}
// NewTestManageableFullIdentity returns a new manageable full identity for use in tests.
func NewTestManageableFullIdentity(ctx context.Context) (*identity.ManageableFullIdentity, error) {
ca, err := NewTestCA(ctx)
if err != nil {
return nil, err
}
ident, err := ca.NewIdentity()
if err != nil {
return nil, err
}
return identity.NewManageableFullIdentity(ident, ca), nil
}

View File

@ -33,9 +33,9 @@ func NewCertChain(length int) (keys []crypto.PrivateKey, certs []*x509.Certifica
var cert *x509.Certificate
if i == 0 {
cert, err = peertls.NewCert(key, nil, template, nil)
cert, err = peertls.NewSelfSignedCert(key, template)
} else {
cert, err = peertls.NewCert(key, keys[i-1], template, certs[i-1:][0])
cert, err = peertls.NewCert(pkcrypto.PublicKeyFromPrivate(key), keys[i-1], template, certs[i-1:][0])
}
if err != nil {
return nil, nil, err

View File

@ -8,50 +8,84 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"storj.io/storj/pkg/identity"
"storj.io/storj/pkg/peertls"
"storj.io/storj/pkg/peertls/extensions"
"storj.io/storj/pkg/pkcrypto"
"storj.io/storj/pkg/peertls/tlsopts"
)
// RevokeLeaf revokes the leaf certificate in the passed chain and replaces it
// with a "revoking" certificate, which contains a revocation extension recording
// this action.
func RevokeLeaf(keys []crypto.PrivateKey, chain []*x509.Certificate) ([]*x509.Certificate, pkix.Extension, error) {
var revocation pkix.Extension
revokingKey, err := pkcrypto.GeneratePrivateKey()
if err != nil {
return nil, revocation, err
func RevokeLeaf(caKey crypto.PrivateKey, chain []*x509.Certificate) ([]*x509.Certificate, pkix.Extension, error) {
if len(chain) < 2 {
return nil, pkix.Extension{}, extensions.Error.New("revoking leaf implies a CA exists; chain too short")
}
ca := &identity.FullCertificateAuthority{
Key: caKey,
Cert: chain[peertls.CAIndex],
RestChain: chain[:peertls.CAIndex+1],
}
revokingTemplate, err := peertls.LeafTemplate()
if err != nil {
return nil, revocation, err
}
revokingCert, err := peertls.NewCert(revokingKey, keys[0], revokingTemplate, chain[peertls.CAIndex])
if err != nil {
return nil, revocation, err
}
err = extensions.AddRevocationExt(keys[0], chain[peertls.LeafIndex], revokingCert)
if err != nil {
return nil, revocation, err
}
revocation = revokingCert.ExtraExtensions[0]
return append([]*x509.Certificate{revokingCert}, chain[peertls.CAIndex:]...), revocation, nil
}
// RevokeCA revokes the CA certificate in the passed chain and adds a revocation
// extension to that certificate, recording this action.
func RevokeCA(keys []crypto.PrivateKey, chain []*x509.Certificate) ([]*x509.Certificate, pkix.Extension, error) {
caCert := chain[peertls.CAIndex]
err := extensions.AddRevocationExt(keys[0], caCert, caCert)
var err error
ca.ID, err = identity.NodeIDFromKey(ca.Cert.PublicKey)
if err != nil {
return nil, pkix.Extension{}, err
}
return append([]*x509.Certificate{caCert}, chain[peertls.CAIndex:]...), caCert.ExtraExtensions[0], nil
ident := &identity.FullIdentity{
Leaf: chain[peertls.LeafIndex],
CA: ca.Cert,
ID: ca.ID,
RestChain: ca.RestChain,
}
manageableIdent := identity.NewManageableFullIdentity(ident, ca)
if err := manageableIdent.Revoke(); err != nil {
return nil, pkix.Extension{}, err
}
revokingCert := manageableIdent.Leaf
var revocationExt *pkix.Extension
for _, ext := range revokingCert.Extensions {
if extensions.RevocationExtID.Equal(ext.Id) {
revocationExt = &ext
break
}
}
if revocationExt == nil {
return nil, pkix.Extension{}, extensions.ErrRevocation.New("no revocation extension found")
}
return append([]*x509.Certificate{revokingCert}, chain[peertls.CAIndex:]...), *revocationExt, nil
}
// RevokeCA revokes the CA certificate in the passed chain and adds a revocation
// extension to that certificate, recording this action.
func RevokeCA(caKey crypto.PrivateKey, chain []*x509.Certificate) ([]*x509.Certificate, pkix.Extension, error) {
caCert := chain[peertls.CAIndex]
nodeID, err := identity.NodeIDFromKey(caCert.PublicKey)
if err != nil {
return nil, pkix.Extension{}, err
}
ca := &identity.FullCertificateAuthority{
ID: nodeID,
Cert: caCert,
Key: caKey,
RestChain: chain[peertls.CAIndex+1:],
}
if err = ca.Revoke(); err != nil {
return nil, pkix.Extension{}, err
}
extMap := tlsopts.NewExtensionsMap(ca.Cert)
revocationExt, ok := extMap[extensions.RevocationExtID.String()]
if !ok {
return nil, pkix.Extension{}, extensions.ErrRevocation.New("no revocation extension found")
}
return append([]*x509.Certificate{chain[peertls.LeafIndex], ca.Cert}, ca.RestChain...), revocationExt, nil
}
// NewRevokedLeafChain creates a certificate chain (of length 2) with a leaf
@ -62,6 +96,6 @@ func NewRevokedLeafChain() ([]crypto.PrivateKey, []*x509.Certificate, pkix.Exten
return nil, nil, pkix.Extension{}, err
}
newChain, revocation, err := RevokeLeaf(keys, certs)
newChain, revocation, err := RevokeLeaf(keys[0], certs)
return keys, newChain, revocation, err
}

View File

@ -1,22 +1,30 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package identity
package identity_test
import (
"context"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"math/rand"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"storj.io/storj/internal/testcontext"
"storj.io/storj/internal/testidentity"
"storj.io/storj/pkg/identity"
"storj.io/storj/pkg/peertls/extensions"
"storj.io/storj/pkg/peertls/tlsopts"
)
func TestNewCA(t *testing.T) {
const expectedDifficulty = 4
ca, err := NewCA(context.Background(), NewCAOptions{
ca, err := identity.NewCA(context.Background(), identity.NewCAOptions{
Difficulty: expectedDifficulty,
Concurrency: 5,
})
@ -30,7 +38,7 @@ func TestNewCA(t *testing.T) {
func TestFullCertificateAuthority_NewIdentity(t *testing.T) {
ctx := testcontext.New(t)
ca, err := NewCA(ctx, NewCAOptions{
ca, err := identity.NewCA(ctx, identity.NewCAOptions{
Difficulty: 12,
Concurrency: 4,
})
@ -54,17 +62,17 @@ func TestFullCertificateAuthority_NewIdentity(t *testing.T) {
func TestFullCertificateAuthority_Sign(t *testing.T) {
ctx := testcontext.New(t)
caOpts := NewCAOptions{
caOpts := identity.NewCAOptions{
Difficulty: 12,
Concurrency: 4,
}
ca, err := NewCA(ctx, caOpts)
ca, err := identity.NewCA(ctx, caOpts)
if !assert.NoError(t, err) || !assert.NotNil(t, ca) {
t.Fatal(err)
}
toSign, err := NewCA(ctx, caOpts)
toSign, err := identity.NewCA(ctx, caOpts)
if !assert.NoError(t, err) || !assert.NotNil(t, toSign) {
t.Fatal(err)
}
@ -96,7 +104,7 @@ func BenchmarkNewCA(b *testing.B) {
test := fmt.Sprintf("%d/%d", difficulty, concurrency)
b.Run(test, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = NewCA(ctx, NewCAOptions{
_, _ = identity.NewCA(ctx, identity.NewCAOptions{
Difficulty: difficulty,
Concurrency: concurrency,
})
@ -105,3 +113,71 @@ func BenchmarkNewCA(b *testing.B) {
}
}
}
func TestFullCertificateAuthority_AddExtension(t *testing.T) {
ctx := testcontext.New(t)
defer ctx.Cleanup()
ca, err := testidentity.NewTestCA(ctx)
require.NoError(t, err)
oldCert := ca.Cert
assert.Len(t, ca.Cert.ExtraExtensions, 0)
randBytes := make([]byte, 10)
rand.Read(randBytes)
randExt := pkix.Extension{
Id: asn1.ObjectIdentifier{2, 999, int(randBytes[0])},
Value: randBytes,
}
err = ca.AddExtension(randExt)
assert.NoError(t, err)
assert.Len(t, ca.Cert.ExtraExtensions, 0)
assert.Len(t, ca.Cert.Extensions, len(oldCert.Extensions)+1)
assert.Equal(t, oldCert.SerialNumber, ca.Cert.SerialNumber)
assert.Equal(t, oldCert.IsCA, ca.Cert.IsCA)
assert.Equal(t, oldCert.PublicKey, ca.Cert.PublicKey)
assert.Equal(t, randExt, tlsopts.NewExtensionsMap(ca.Cert)[randExt.Id.String()])
assert.NotEqual(t, oldCert.Raw, ca.Cert.Raw)
assert.NotEqual(t, oldCert.RawTBSCertificate, ca.Cert.RawTBSCertificate)
assert.NotEqual(t, oldCert.Signature, ca.Cert.Signature)
}
func TestFullCertificateAuthority_Revoke(t *testing.T) {
ctx := testcontext.New(t)
defer ctx.Cleanup()
ca, err := testidentity.NewTestCA(ctx)
require.NoError(t, err)
oldCert := ca.Cert
assert.Len(t, ca.Cert.ExtraExtensions, 0)
err = ca.Revoke()
assert.NoError(t, err)
assert.Len(t, ca.Cert.ExtraExtensions, 0)
assert.Len(t, ca.Cert.Extensions, len(oldCert.Extensions)+1)
assert.Equal(t, oldCert.SerialNumber, ca.Cert.SerialNumber)
assert.Equal(t, oldCert.IsCA, ca.Cert.IsCA)
assert.Equal(t, oldCert.PublicKey, ca.Cert.PublicKey)
assert.NotEqual(t, oldCert.Raw, ca.Cert.Raw)
assert.NotEqual(t, oldCert.RawTBSCertificate, ca.Cert.RawTBSCertificate)
assert.NotEqual(t, oldCert.Signature, ca.Cert.Signature)
revocationExt := tlsopts.NewExtensionsMap(ca.Cert)[extensions.RevocationExtID.String()]
assert.True(t, extensions.RevocationExtID.Equal(revocationExt.Id))
var rev extensions.Revocation
err = rev.Unmarshal(revocationExt.Value)
require.NoError(t, err)
err = rev.Verify(ca.Cert)
assert.NoError(t, err)
}

View File

@ -9,6 +9,7 @@ import (
"crypto"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"io"
"io/ioutil"
@ -161,7 +162,12 @@ func NewCA(ctx context.Context, opts NewCAOptions) (_ *FullCertificateAuthority,
if err != nil {
return nil, err
}
c, err := peertls.NewCert(selectedKey, opts.ParentKey, ct, opts.ParentCert)
if opts.ParentKey == nil {
opts.ParentKey = selectedKey
}
c, err := peertls.NewCert(pkcrypto.PublicKeyFromPrivate(selectedKey), opts.ParentKey, ct, opts.ParentCert)
if err != nil {
return nil, err
}
@ -350,7 +356,7 @@ func (pc PeerCAConfig) SaveBackup(ca *PeerCertificateAuthority) error {
// NewIdentity generates a new `FullIdentity` based on the CA. The CA
// cert is included in the identity's cert chain and the identity's leaf cert
// is signed by the CA.
func (ca *FullCertificateAuthority) NewIdentity() (*FullIdentity, error) {
func (ca *FullCertificateAuthority) NewIdentity(exts ...pkix.Extension) (*FullIdentity, error) {
leafTemplate, err := peertls.LeafTemplate()
if err != nil {
return nil, err
@ -359,16 +365,14 @@ func (ca *FullCertificateAuthority) NewIdentity() (*FullIdentity, error) {
if err != nil {
return nil, err
}
leafCert, err := peertls.NewCert(leafKey, ca.Key, leafTemplate, ca.Cert)
if err != nil {
if err := extensions.AddExtraExtension(leafTemplate, exts...); err != nil {
return nil, err
}
if ca.RestChain != nil && len(ca.RestChain) > 0 {
err := extensions.AddSignedCert(ca.Key, leafCert)
if err != nil {
return nil, err
}
leafCert, err := peertls.NewCert(pkcrypto.PublicKeyFromPrivate(leafKey), ca.Key, leafTemplate, ca.Cert)
if err != nil {
return nil, err
}
return &FullIdentity{
@ -428,3 +432,34 @@ func (ca *FullCertificateAuthority) Sign(cert *x509.Certificate) (*x509.Certific
return signedCert, nil
}
// AddExtension adds extensions to certificate authority certificate. Extensions
// are serialized into the certificate's raw bytes and it is re-signed by itself.
func (ca *FullCertificateAuthority) AddExtension(ext ...pkix.Extension) error {
// TODO: how to properly handle this?
if len(ca.RestChain) > 0 {
return errs.New("adding extensions requires parent certificate's private key")
}
if err := extensions.AddExtraExtension(ca.Cert, ext...); err != nil {
return err
}
updatedCert, err := peertls.NewSelfSignedCert(ca.Key, ca.Cert)
if err != nil {
return err
}
ca.Cert = updatedCert
return nil
}
// Revoke extends the certificate authority certificate with a certificate revocation extension.
func (ca *FullCertificateAuthority) Revoke() error {
ext, err := extensions.NewRevocationExt(ca.Key, ca.Cert)
if err != nil {
return err
}
return ca.AddExtension(ext)
}

View File

@ -7,8 +7,8 @@ import (
"bytes"
"context"
"crypto"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"io/ioutil"
"path/filepath"
@ -21,6 +21,7 @@ import (
"google.golang.org/grpc/peer"
"storj.io/storj/pkg/peertls"
"storj.io/storj/pkg/peertls/extensions"
"storj.io/storj/pkg/pkcrypto"
"storj.io/storj/pkg/storj"
)
@ -52,6 +53,22 @@ type FullIdentity struct {
Key crypto.PrivateKey
}
// ManageablePeerIdentity is a `PeerIdentity` and its corresponding `FullCertificateAuthority`
// in a single struct. It is used for making changes to the identity that require CA
// authorization; e.g. adding extensions.
type ManageablePeerIdentity struct {
*PeerIdentity
CA *FullCertificateAuthority
}
// ManageableFullIdentity is a `FullIdentity` and its corresponding `FullCertificateAuthority`
// in a single struct. It is used for making changes to the identity that require CA
// authorization and the leaf private key; e.g. revoking a leaf cert (private key changes).
type ManageableFullIdentity struct {
*FullIdentity
CA *FullCertificateAuthority
}
// SetupConfig allows you to run a set of Responsibilities with the given
// identity. You can also just load an Identity from disk.
type SetupConfig struct {
@ -234,14 +251,11 @@ func NodeIDFromPEM(pemBytes []byte) (storj.NodeID, error) {
// NodeIDFromKey hashes a public key and creates a node ID from it
func NodeIDFromKey(k crypto.PublicKey) (storj.NodeID, error) {
// id = sha256(sha256(pkix(k)))
kb, err := x509.MarshalPKIXPublicKey(k)
idBytes, err := peertls.DoubleSHA256PublicKey(k)
if err != nil {
return storj.NodeID{}, storj.ErrNodeID.Wrap(err)
}
mid := sha256.Sum256(kb)
end := sha256.Sum256(mid[:])
return storj.NodeID(end), nil
return storj.NodeID(idBytes), nil
}
// NewFullIdentity creates a new ID for nodes with difficulty and concurrency params
@ -269,6 +283,22 @@ func ToChains(chains ...[]*x509.Certificate) [][]*x509.Certificate {
return combinedChains
}
// NewManageablePeerIdentity returns a manageable identity given a full identity and a full certificate authority.
func NewManageablePeerIdentity(ident *PeerIdentity, ca *FullCertificateAuthority) *ManageablePeerIdentity {
return &ManageablePeerIdentity{
PeerIdentity: ident,
CA: ca,
}
}
// NewManageableFullIdentity returns a manageable identity given a full identity and a full certificate authority.
func NewManageableFullIdentity(ident *FullIdentity, ca *FullCertificateAuthority) *ManageableFullIdentity {
return &ManageableFullIdentity{
FullIdentity: ident,
CA: ca,
}
}
// Status returns the status of the identity cert/key files for the config
func (is SetupConfig) Status() (TLSFilesStatus, error) {
return statTLSFiles(is.CertPath, is.KeyPath)
@ -375,14 +405,14 @@ func (ic PeerConfig) Load() (*PeerIdentity, error) {
}
// Save saves a PeerIdentity according to the config
func (ic PeerConfig) Save(fi *PeerIdentity) error {
func (ic PeerConfig) Save(peerIdent *PeerIdentity) error {
var (
certData bytes.Buffer
writeChainErr, writeChainDataErr error
)
chain := []*x509.Certificate{fi.Leaf, fi.CA}
chain = append(chain, fi.RestChain...)
chain := []*x509.Certificate{peerIdent.Leaf, peerIdent.CA}
chain = append(chain, peerIdent.RestChain...)
if ic.CertPath != "" {
writeChainErr = peertls.WriteChain(&certData, chain...)
@ -440,6 +470,40 @@ func (fi *FullIdentity) PeerIdentity() *PeerIdentity {
}
}
// AddExtension adds extensions to the leaf cert of an identity. Extensions
// are serialized into the certificate's raw bytes and is re-signed by it's
// certificate authority.
func (manageableIdent *ManageablePeerIdentity) AddExtension(ext ...pkix.Extension) error {
if err := extensions.AddExtraExtension(manageableIdent.Leaf, ext...); err != nil {
return err
}
updatedCert, err := peertls.NewCert(manageableIdent.Leaf.PublicKey, manageableIdent.CA.Key, manageableIdent.Leaf, manageableIdent.CA.Cert)
if err != nil {
return err
}
manageableIdent.Leaf = updatedCert
return nil
}
// Revoke extends the CA certificate with a certificate revocation extension.
func (manageableIdent *ManageableFullIdentity) Revoke() error {
ext, err := extensions.NewRevocationExt(manageableIdent.CA.Key, manageableIdent.Leaf)
if err != nil {
return err
}
revokingIdent, err := manageableIdent.CA.NewIdentity(ext)
if err != nil {
return err
}
manageableIdent.Leaf = revokingIdent.Leaf
return nil
}
func backupPath(path string) string {
pathExt := filepath.Ext(path)
base := strings.TrimSuffix(path, pathExt)

View File

@ -6,15 +6,22 @@ package identity_test
import (
"bytes"
"context"
"crypto/rand"
"crypto/x509/pkix"
"encoding/asn1"
"os"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"storj.io/storj/internal/testcontext"
"storj.io/storj/internal/testidentity"
"storj.io/storj/pkg/identity"
"storj.io/storj/pkg/peertls"
"storj.io/storj/pkg/peertls/extensions"
"storj.io/storj/pkg/peertls/tlsopts"
"storj.io/storj/pkg/pkcrypto"
)
@ -25,7 +32,7 @@ func TestPeerIdentityFromCertChain(t *testing.T) {
caTemplate, err := peertls.CATemplate()
assert.NoError(t, err)
caCert, err := peertls.NewCert(caKey, caKey, caTemplate, nil)
caCert, err := peertls.NewSelfSignedCert(caKey, caTemplate)
assert.NoError(t, err)
leafTemplate, err := peertls.LeafTemplate()
@ -34,7 +41,7 @@ func TestPeerIdentityFromCertChain(t *testing.T) {
leafKey, err := pkcrypto.GeneratePrivateKey()
assert.NoError(t, err)
leafCert, err := peertls.NewCert(leafKey, caKey, leafTemplate, caTemplate)
leafCert, err := peertls.NewCert(pkcrypto.PublicKeyFromPrivate(leafKey), caKey, leafTemplate, caTemplate)
assert.NoError(t, err)
peerIdent, err := identity.PeerIdentityFromCerts(leafCert, caCert, nil)
@ -51,7 +58,7 @@ func TestFullIdentityFromPEM(t *testing.T) {
caTemplate, err := peertls.CATemplate()
assert.NoError(t, err)
caCert, err := peertls.NewCert(caKey, caKey, caTemplate, nil)
caCert, err := peertls.NewSelfSignedCert(caKey, caTemplate)
assert.NoError(t, err)
assert.NoError(t, err)
assert.NotEmpty(t, caCert)
@ -62,7 +69,7 @@ func TestFullIdentityFromPEM(t *testing.T) {
leafKey, err := pkcrypto.GeneratePrivateKey()
assert.NoError(t, err)
leafCert, err := peertls.NewCert(leafKey, caKey, leafTemplate, caTemplate)
leafCert, err := peertls.NewCert(pkcrypto.PublicKeyFromPrivate(leafKey), caKey, leafTemplate, caTemplate)
assert.NoError(t, err)
assert.NotEmpty(t, leafCert)
@ -141,6 +148,76 @@ func TestVerifyPeer(t *testing.T) {
assert.NoError(t, err)
}
func TestManageablePeerIdentity_AddExtension(t *testing.T) {
ctx := testcontext.New(t)
defer ctx.Cleanup()
manageablePeerIdentity, err := testidentity.NewTestManageablePeerIdentity(ctx)
require.NoError(t, err)
oldLeaf := manageablePeerIdentity.Leaf
assert.Len(t, manageablePeerIdentity.CA.Cert.ExtraExtensions, 0)
randBytes := make([]byte, 10)
_, err = rand.Read(randBytes)
require.NoError(t, err)
randExt := pkix.Extension{
Id: asn1.ObjectIdentifier{2, 999, int(randBytes[0])},
Value: randBytes,
}
err = manageablePeerIdentity.AddExtension(randExt)
assert.NoError(t, err)
assert.Len(t, manageablePeerIdentity.Leaf.ExtraExtensions, 0)
assert.Len(t, manageablePeerIdentity.Leaf.Extensions, len(oldLeaf.Extensions)+1)
assert.Equal(t, oldLeaf.SerialNumber, manageablePeerIdentity.Leaf.SerialNumber)
assert.Equal(t, oldLeaf.IsCA, manageablePeerIdentity.Leaf.IsCA)
assert.Equal(t, oldLeaf.PublicKey, manageablePeerIdentity.Leaf.PublicKey)
assert.Equal(t, randExt, tlsopts.NewExtensionsMap(manageablePeerIdentity.Leaf)[randExt.Id.String()])
assert.NotEqual(t, oldLeaf.Raw, manageablePeerIdentity.Leaf.Raw)
assert.NotEqual(t, oldLeaf.RawTBSCertificate, manageablePeerIdentity.Leaf.RawTBSCertificate)
assert.NotEqual(t, oldLeaf.Signature, manageablePeerIdentity.Leaf.Signature)
}
func TestManageableFullIdentity_Revoke(t *testing.T) {
ctx := testcontext.New(t)
defer ctx.Cleanup()
manageableFullIdentity, err := testidentity.NewTestManageableFullIdentity(ctx)
require.NoError(t, err)
oldLeaf := manageableFullIdentity.Leaf
assert.Len(t, manageableFullIdentity.CA.Cert.ExtraExtensions, 0)
err = manageableFullIdentity.Revoke()
assert.NoError(t, err)
assert.Len(t, manageableFullIdentity.Leaf.ExtraExtensions, 0)
assert.Len(t, manageableFullIdentity.Leaf.Extensions, len(oldLeaf.Extensions)+1)
assert.Equal(t, oldLeaf.IsCA, manageableFullIdentity.Leaf.IsCA)
assert.NotEqual(t, oldLeaf.PublicKey, manageableFullIdentity.Leaf.PublicKey)
assert.NotEqual(t, oldLeaf.SerialNumber, manageableFullIdentity.Leaf.SerialNumber)
assert.NotEqual(t, oldLeaf.Raw, manageableFullIdentity.Leaf.Raw)
assert.NotEqual(t, oldLeaf.RawTBSCertificate, manageableFullIdentity.Leaf.RawTBSCertificate)
assert.NotEqual(t, oldLeaf.Signature, manageableFullIdentity.Leaf.Signature)
revocationExt := tlsopts.NewExtensionsMap(manageableFullIdentity.Leaf)[extensions.RevocationExtID.String()]
assert.True(t, extensions.RevocationExtID.Equal(revocationExt.Id))
var rev extensions.Revocation
err = rev.Unmarshal(revocationExt.Value)
require.NoError(t, err)
err = rev.Verify(manageableFullIdentity.CA.Cert)
assert.NoError(t, err)
}
func pregeneratedIdentity(t *testing.T) *identity.FullIdentity {
const chain = `-----BEGIN CERTIFICATE-----
MIIBQDCB56ADAgECAhB+u3d03qyW/ROgwy/ZsPccMAoGCCqGSM49BAMCMAAwIhgP

View File

@ -117,7 +117,7 @@ func AddSignedCert(key crypto.PrivateKey, cert *x509.Certificate) error {
return err
}
err = AddExtension(cert, pkix.Extension{
err = AddExtraExtension(cert, pkix.Extension{
Id: SignedCertExtID,
Value: signature,
})
@ -127,8 +127,11 @@ func AddSignedCert(key crypto.PrivateKey, cert *x509.Certificate) error {
return nil
}
// AddExtension adds one or more extensions to a certificate
func AddExtension(cert *x509.Certificate, exts ...pkix.Extension) (err error) {
// AddExtraExtension adds one or more extensions to a certificate for serialization.
// NB: this *does not* serialize or persist the extension into the certificates's
// raw bytes. To add a persistent extension use `FullCertificateAuthority.AddExtension`
// or `ManageableIdentity.AddExtension`.
func AddExtraExtension(cert *x509.Certificate, exts ...pkix.Extension) (err error) {
if len(exts) == 0 {
return nil
}

View File

@ -8,169 +8,16 @@ import (
"crypto/x509/pkix"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zeebo/errs"
"storj.io/storj/internal/testcontext"
"storj.io/storj/internal/testpeertls"
"storj.io/storj/pkg/identity"
"storj.io/storj/pkg/peertls/extensions"
"storj.io/storj/pkg/peertls/tlsopts"
)
func TestParseExtensions(t *testing.T) {
// TODO: separate this into multiple tests!
// TODO: this is not a great test
ctx := testcontext.New(t)
defer ctx.Cleanup()
revokedLeafKeys, revokedLeafChain, _, err := testpeertls.NewRevokedLeafChain()
assert.NoError(t, err)
whitelistSignedKeys, whitelistSignedChain, err := testpeertls.NewCertChain(3)
assert.NoError(t, err)
err = extensions.AddSignedCert(whitelistSignedKeys[0], whitelistSignedChain[0])
assert.NoError(t, err)
_, unrelatedChain, err := testpeertls.NewCertChain(1)
assert.NoError(t, err)
revDB, err := identity.NewRevocationDB("bolt://" + ctx.File("revocations.db"))
assert.NoError(t, err)
defer ctx.Check(revDB.Close)
testcases := []struct {
name string
config extensions.Config
certChain []*x509.Certificate
whitelist []*x509.Certificate
errClass *errs.Class
err error
}{
{
"leaf whitelist signature - success",
extensions.Config{WhitelistSignedLeaf: true},
whitelistSignedChain,
[]*x509.Certificate{whitelistSignedChain[2]},
nil,
nil,
},
{
"leaf whitelist signature - failure (empty whitelist)",
extensions.Config{WhitelistSignedLeaf: true},
whitelistSignedChain,
nil,
&extensions.Error,
nil,
},
{
"leaf whitelist signature - failure",
extensions.Config{WhitelistSignedLeaf: true},
whitelistSignedChain,
unrelatedChain,
&extensions.Error,
nil,
},
{
"certificate revocation - single revocation ",
extensions.Config{Revocation: true},
revokedLeafChain,
nil,
nil,
nil,
},
{
"certificate revocation - serial revocations",
extensions.Config{Revocation: true},
func() []*x509.Certificate {
rev := new(extensions.Revocation)
time.Sleep(1 * time.Second)
chain, revocationExt, err := testpeertls.RevokeLeaf(revokedLeafKeys, revokedLeafChain)
assert.NoError(t, err)
err = rev.Unmarshal(revocationExt.Value)
assert.NoError(t, err)
return chain
}(),
nil,
nil,
nil,
},
{
"certificate revocation - serial revocations error (older timestamp)",
extensions.Config{Revocation: true},
func() []*x509.Certificate {
keys, chain, _, err := testpeertls.NewRevokedLeafChain()
assert.NoError(t, err)
rev := new(extensions.Revocation)
err = rev.Unmarshal(chain[0].ExtraExtensions[0].Value)
assert.NoError(t, err)
rev.Timestamp = rev.Timestamp + 300
err = rev.Sign(keys[0])
assert.NoError(t, err)
revBytes, err := rev.Marshal()
assert.NoError(t, err)
err = revDB.Put(chain, pkix.Extension{
Id: extensions.RevocationExtID,
Value: revBytes,
})
assert.NoError(t, err)
return chain
}(),
nil,
&extensions.Error,
extensions.ErrRevocationTimestamp,
},
{
"certificate revocation and leaf whitelist signature",
extensions.Config{Revocation: true, WhitelistSignedLeaf: true},
func() []*x509.Certificate {
_, chain, _, err := testpeertls.NewRevokedLeafChain()
assert.NoError(t, err)
err = extensions.AddSignedCert(whitelistSignedKeys[0], chain[0])
assert.NoError(t, err)
return chain
}(),
[]*x509.Certificate{whitelistSignedChain[2]},
nil,
nil,
},
}
for _, testcase := range testcases {
t.Run(testcase.name, func(t *testing.T) {
opts := &extensions.Options{
PeerCAWhitelist: testcase.whitelist,
RevDB: revDB,
}
handlerFuncMap := extensions.AllHandlers.WithOptions(opts)
extensionsMap := tlsopts.NewExtensionsMap(testcase.certChain...)
err := extensionsMap.HandleExtensions(handlerFuncMap, identity.ToChains(testcase.certChain))
if testcase.errClass != nil {
assert.True(t, testcase.errClass.Has(err))
}
if testcase.err != nil {
assert.NotNil(t, err)
}
if testcase.errClass == nil && testcase.err == nil {
assert.NoError(t, err)
}
})
}
}
func TestHandlers_Register(t *testing.T) {
var (
handlers = extensions.HandlerFactories{}

View File

@ -40,10 +40,10 @@ var ErrRevokedCert = ErrRevocation.New("a certificate in the chain is revoked")
var ErrRevocationTimestamp = Error.New("revocation timestamp is older than last known revocation")
// Revocation represents a certificate revocation for storage in the revocation
// database and for use in a TLS extension
// database and for use in a TLS extension.
type Revocation struct {
Timestamp int64
CertHash []byte
KeyHash []byte
Signature []byte
}
@ -67,10 +67,13 @@ func init() {
func NewRevocationExt(key crypto.PrivateKey, revokedCert *x509.Certificate) (pkix.Extension, error) {
nowUnix := time.Now().Unix()
hash := pkcrypto.SHA256Hash(revokedCert.Raw)
keyHash, err := peertls.DoubleSHA256PublicKey(revokedCert.PublicKey)
if err != nil {
return pkix.Extension{}, err
}
rev := Revocation{
Timestamp: nowUnix,
CertHash: hash,
KeyHash: keyHash[:],
}
if err := rev.Sign(key); err != nil {
@ -90,24 +93,9 @@ func NewRevocationExt(key crypto.PrivateKey, revokedCert *x509.Certificate) (pki
return ext, nil
}
// AddRevocationExt generates a revocation extension for a cert and attaches it
// to the cert which will replace the revoked cert.
func AddRevocationExt(key crypto.PrivateKey, revokedCert, newCert *x509.Certificate) error {
ext, err := NewRevocationExt(key, revokedCert)
if err != nil {
return err
}
err = AddExtension(newCert, ext)
if err != nil {
return err
}
return nil
}
func revocationChecker(opts *Options) HandlerFunc {
return func(_ pkix.Extension, chains [][]*x509.Certificate) error {
leaf := chains[0][peertls.LeafIndex]
ca := chains[0][peertls.CAIndex]
ca, leaf := chains[0][peertls.CAIndex], chains[0][peertls.LeafIndex]
lastRev, lastRevErr := opts.RevDB.Get(chains[0])
if lastRevErr != nil {
return Error.Wrap(lastRevErr)
@ -116,15 +104,21 @@ func revocationChecker(opts *Options) HandlerFunc {
return nil
}
caHash := pkcrypto.SHA256Hash(ca.Raw)
leafHash := pkcrypto.SHA256Hash(leaf.Raw)
nodeID, err := peertls.DoubleSHA256PublicKey(ca.PublicKey)
if err != nil {
return err
}
leafKeyHash, err := peertls.DoubleSHA256PublicKey(leaf.PublicKey)
if err != nil {
return err
}
// NB: we trust that anything that made it into the revocation DB is valid
// (i.e. no need for further verification)
switch {
case bytes.Equal(lastRev.CertHash, caHash):
return ErrRevokedCert
case bytes.Equal(lastRev.CertHash, leafHash):
case bytes.Equal(lastRev.KeyHash, nodeID[:]):
fallthrough
case bytes.Equal(lastRev.KeyHash, leafKeyHash[:]):
return ErrRevokedCert
default:
return nil
@ -155,12 +149,12 @@ func (r Revocation) Verify(signingCert *x509.Certificate) error {
return nil
}
// TBSBytes (ToBeSigned) returns the hash of the revoked certificate hash and
// the timestamp (i.e. hash(hash(cert bytes) + timestamp)).
// TBSBytes (ToBeSigned) returns the hash of the revoked certificate key hash
// and the timestamp (i.e. hash(hash(cert bytes) + timestamp)).
func (r *Revocation) TBSBytes() []byte {
var tsBytes [binary.MaxVarintLen64]byte
binary.PutVarint(tsBytes[:], r.Timestamp)
toHash := append(append([]byte{}, r.CertHash...), tsBytes[:]...)
toHash := append(append([]byte{}, r.KeyHash...), tsBytes[:]...)
return pkcrypto.SHA256Hash(toHash)
}

View File

@ -11,7 +11,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"storj.io/storj/internal/testcontext"
"storj.io/storj/internal/testidentity"
"storj.io/storj/internal/testpeertls"
"storj.io/storj/pkg/identity"
@ -21,9 +20,6 @@ import (
)
func TestRevocationCheckHandler(t *testing.T) {
ctx := testcontext.New(t)
defer ctx.Cleanup()
testidentity.RevocationDBsTest(t, func(t *testing.T, revDB extensions.RevocationDB, _ storage.KeyValueStore) {
keys, chain, err := testpeertls.NewCertChain(2)
assert.NoError(t, err)
@ -37,68 +33,97 @@ func TestRevocationCheckHandler(t *testing.T) {
assert.NoError(t, err)
}
revokedLeafChain, leafRevocationExt, err := testpeertls.RevokeLeaf(keys, chain)
revokingChain, leafRevocationExt, err := testpeertls.RevokeLeaf(keys[0], chain)
require.NoError(t, err)
assert.Equal(t, chain[peertls.CAIndex].Raw, revokedLeafChain[peertls.CAIndex].Raw)
assert.Equal(t, chain[peertls.CAIndex].Raw, revokingChain[peertls.CAIndex].Raw)
{
t.Log("revoked leaf success")
err := revocationChecker(pkix.Extension{}, identity.ToChains(revokedLeafChain))
t.Log("revoked leaf success (original chain)")
err := revocationChecker(pkix.Extension{}, identity.ToChains(chain))
assert.NoError(t, err)
}
{
t.Log("revoked leaf success (revoking chain)")
err := revocationChecker(pkix.Extension{}, identity.ToChains(revokingChain))
assert.NoError(t, err)
}
// NB: add leaf revocation to revocation DB
err = revDB.Put(revokedLeafChain, leafRevocationExt)
t.Log("revocation DB put leaf revocation")
err = revDB.Put(revokingChain, leafRevocationExt)
require.NoError(t, err)
{
t.Log("revoked leaf error")
t.Log("revoked leaf success (revoking chain)")
err := revocationChecker(pkix.Extension{}, identity.ToChains(revokingChain))
assert.NoError(t, err)
}
{
t.Log("revoked leaf error (original chain)")
err := revocationChecker(pkix.Extension{}, identity.ToChains(chain))
assert.Error(t, err)
}
})
// NB: timestamp must be different because the NodeID is the same
time.Sleep(time.Second)
testidentity.RevocationDBsTest(t, func(t *testing.T, revDB extensions.RevocationDB, _ storage.KeyValueStore) {
keys, chain, err := testpeertls.NewCertChain(2)
assert.NoError(t, err)
revokedCAChain, caRevocationExt, err := testpeertls.RevokeCA(keys, chain)
opts := &extensions.Options{RevDB: revDB}
revocationChecker := extensions.RevocationCheckHandler.NewHandlerFunc(opts)
revokingChain, caRevocationExt, err := testpeertls.RevokeCA(keys[0], chain)
require.NoError(t, err)
assert.NotEqual(t, chain[peertls.CAIndex].Raw, revokingChain[peertls.CAIndex].Raw)
{
t.Log("revoked CA success")
err := revocationChecker(pkix.Extension{}, identity.ToChains(revokedCAChain))
t.Log("revoked CA error (original chain)")
err := revocationChecker(pkix.Extension{}, identity.ToChains(chain))
assert.NoError(t, err)
}
{
t.Log("revoked CA success (revokingChain)")
err := revocationChecker(pkix.Extension{}, identity.ToChains(revokingChain))
assert.NoError(t, err)
}
// NB: add CA revocation to revocation DB
err = revDB.Put(revokedCAChain, caRevocationExt)
t.Log("revocation DB put CA revocation")
err = revDB.Put(revokingChain, caRevocationExt)
require.NoError(t, err)
{
t.Log("revoked CA error")
err := revocationChecker(pkix.Extension{}, identity.ToChains(revokedCAChain))
t.Log("revoked CA error (revoking CA chain)")
err := revocationChecker(pkix.Extension{}, identity.ToChains(revokingChain))
assert.Error(t, err)
}
{
t.Log("revoked CA error (original chain)")
err := revocationChecker(pkix.Extension{}, identity.ToChains(chain))
assert.Error(t, err)
}
})
}
func TestRevocationUpdateHandler(t *testing.T) {
ctx := testcontext.New(t)
defer ctx.Cleanup()
testidentity.RevocationDBsTest(t, func(t *testing.T, revDB extensions.RevocationDB, _ storage.KeyValueStore) {
keys, chain, err := testpeertls.NewCertChain(2)
assert.NoError(t, err)
olderRevokedChain, olderRevocation, err := testpeertls.RevokeLeaf(keys, chain)
olderRevokedChain, olderRevocation, err := testpeertls.RevokeLeaf(keys[0], chain)
require.NoError(t, err)
time.Sleep(time.Second)
revokedLeafChain, newerRevocation, err := testpeertls.RevokeLeaf(keys, chain)
revokedLeafChain, newerRevocation, err := testpeertls.RevokeLeaf(keys[0], chain)
require.NoError(t, err)
time.Sleep(time.Second)
newestRevokedChain, newestRevocation, err := testpeertls.RevokeLeaf(keys, revokedLeafChain)
newestRevokedChain, newestRevocation, err := testpeertls.RevokeLeaf(keys[0], revokedLeafChain)
require.NoError(t, err)
opts := &extensions.Options{RevDB: revDB}

View File

@ -6,6 +6,7 @@ package peertls // import "storj.io/storj/pkg/peertls"
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/tls"
"crypto/x509"
@ -114,12 +115,6 @@ func WriteChain(w io.Writer, chain ...*x509.Certificate) error {
if err := pkcrypto.WriteCertPEM(w, c); err != nil {
return errs.Wrap(err)
}
for _, e := range c.ExtraExtensions {
if err := pkcrypto.WritePKIXExtensionPEM(w, &e); err != nil {
extErrs.Add(errs.Wrap(err))
}
}
}
return extErrs.Err()
}
@ -131,26 +126,29 @@ func ChainBytes(chain ...*x509.Certificate) ([]byte, error) {
return data.Bytes(), err
}
// NewSelfSignedCert returns a new x509 self-signed certificate using the provided // template and key,
func NewSelfSignedCert(key crypto.PrivateKey, template *x509.Certificate) (*x509.Certificate, error) {
return NewCert(pkcrypto.PublicKeyFromPrivate(key), key, template, template)
}
// NewCert returns a new x509 certificate using the provided templates and key,
// signed by the parent cert if provided; otherwise, self-signed.
func NewCert(key, parentKey crypto.PrivateKey, template, parent *x509.Certificate) (*x509.Certificate, error) {
var signingKey crypto.PrivateKey
if parentKey != nil {
signingKey = parentKey
} else {
signingKey = key
}
func NewCert(publicKey crypto.PublicKey, parentKey crypto.PrivateKey, template, parent *x509.Certificate) (*x509.Certificate, error) {
if parent == nil {
parent = template
}
publicECKey, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
return nil, errs.New("unsupported public key type %T", publicKey)
}
cb, err := x509.CreateCertificate(
rand.Reader,
template,
parent,
pkcrypto.PublicKeyFromPrivate(key),
signingKey,
publicECKey,
parentKey,
)
if err != nil {
return nil, errs.Wrap(err)

View File

@ -30,7 +30,7 @@ func TestNewCert_CA(t *testing.T) {
caTemplate, err := peertls.CATemplate()
assert.NoError(t, err)
caCert, err := peertls.NewCert(caKey, nil, caTemplate, nil)
caCert, err := peertls.NewSelfSignedCert(caKey, caTemplate)
assert.NoError(t, err)
assert.NotEmpty(t, caKey)
@ -48,7 +48,7 @@ func TestNewCert_Leaf(t *testing.T) {
caTemplate, err := peertls.CATemplate()
assert.NoError(t, err)
caCert, err := peertls.NewCert(caKey, nil, caTemplate, nil)
caCert, err := peertls.NewSelfSignedCert(caKey, caTemplate)
assert.NoError(t, err)
leafKey, err := pkcrypto.GeneratePrivateKey()
@ -57,7 +57,8 @@ func TestNewCert_Leaf(t *testing.T) {
leafTemplate, err := peertls.LeafTemplate()
assert.NoError(t, err)
leafCert, err := peertls.NewCert(leafKey, caKey, leafTemplate, caCert)
leafPublicKey := pkcrypto.PublicKeyFromPrivate(leafKey)
leafCert, err := peertls.NewCert(leafPublicKey, caKey, leafTemplate, caCert)
assert.NoError(t, err)
assert.NotEmpty(t, caKey)
@ -110,7 +111,7 @@ func TestVerifyPeerCertChains(t *testing.T) {
wrongKey, err := pkcrypto.GeneratePrivateKey()
assert.NoError(t, err)
leafCert, err = peertls.NewCert(leafKey, wrongKey, leafCert, caCert)
leafCert, err = peertls.NewCert(pkcrypto.PublicKeyFromPrivate(leafKey), wrongKey, leafCert, caCert)
assert.NoError(t, err)
err = peertls.VerifyPeerFunc(peertls.VerifyPeerCertChains)([][]byte{leafCert.Raw, caCert.Raw}, nil)
@ -177,70 +178,30 @@ func TestVerifyCAWhitelist(t *testing.T) {
})
}
func TestAddExtension(t *testing.T) {
func TestAddExtraExtension(t *testing.T) {
_, chain, err := testpeertls.NewCertChain(1)
if !assert.NoError(t, err) {
t.FailNow()
}
require.NoError(t, err)
// NB: there's nothing special about length 32
randBytes := make([]byte, 32)
exampleID := asn1.ObjectIdentifier{2, 999}
i, err := rand.Read(randBytes)
cert := chain[0]
extLen := len(cert.Extensions)
randBytes := make([]byte, 10)
_, err = rand.Read(randBytes)
require.NoError(t, err)
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Equal(t, 32, i)
ext := pkix.Extension{
Id: exampleID,
Id: asn1.ObjectIdentifier{2, 999, int(randBytes[0])},
Value: randBytes,
}
err = extensions.AddExtension(chain[0], ext)
assert.NoError(t, err)
assert.Len(t, chain[0].ExtraExtensions, 1)
assert.Equal(t, ext, chain[0].ExtraExtensions[0])
}
func TestAddSignedCertExt(t *testing.T) {
keys, chain, err := testpeertls.NewCertChain(1)
if !assert.NoError(t, err) {
t.FailNow()
}
err = extensions.AddSignedCert(keys[0], chain[0])
assert.NoError(t, err)
assert.Len(t, chain[0].ExtraExtensions, 1)
assert.True(t, extensions.SignedCertExtID.Equal(chain[0].ExtraExtensions[0].Id))
err = pkcrypto.HashAndVerifySignature(
pkcrypto.PublicKeyFromPrivate(keys[0]),
chain[0].RawTBSCertificate,
chain[0].ExtraExtensions[0].Value,
)
assert.NoError(t, err)
}
func TestSignLeafExt(t *testing.T) {
keys, chain, err := testpeertls.NewCertChain(2)
if !assert.NoError(t, err) {
t.FailNow()
}
caKey, leafCert := keys[0], chain[0]
err = extensions.AddSignedCert(caKey, leafCert)
assert.NoError(t, err)
assert.Equal(t, 1, len(leafCert.ExtraExtensions))
assert.True(t, extensions.SignedCertExtID.Equal(leafCert.ExtraExtensions[0].Id))
err = pkcrypto.HashAndVerifySignature(
pkcrypto.PublicKeyFromPrivate(caKey),
leafCert.RawTBSCertificate,
leafCert.ExtraExtensions[0].Value,
)
err = extensions.AddExtraExtension(cert, ext)
assert.NoError(t, err)
assert.Len(t, cert.ExtraExtensions, 1)
require.Len(t, cert.Extensions, extLen)
assert.Equal(t, ext, cert.ExtraExtensions[0])
}
func TestRevocation_Sign(t *testing.T) {
@ -248,13 +209,14 @@ func TestRevocation_Sign(t *testing.T) {
assert.NoError(t, err)
leafCert, caKey := chain[0], keys[0]
leafHash := pkcrypto.SHA256Hash(leafCert.Raw)
leafKeyHash, err := peertls.DoubleSHA256PublicKey(leafCert.PublicKey)
require.NoError(t, err)
rev := extensions.Revocation{
Timestamp: time.Now().Unix(),
CertHash: make([]byte, len(leafHash)),
KeyHash: make([]byte, len(leafKeyHash)),
}
copy(rev.CertHash, leafHash)
copy(rev.KeyHash, leafKeyHash[:])
err = rev.Sign(caKey)
assert.NoError(t, err)
assert.NotEmpty(t, rev.Signature)
@ -265,13 +227,14 @@ func TestRevocation_Verify(t *testing.T) {
assert.NoError(t, err)
leafCert, caCert, caKey := chain[0], chain[1], keys[0]
leafHash := pkcrypto.SHA256Hash(leafCert.Raw)
leafKeyHash, err := peertls.DoubleSHA256PublicKey(leafCert.PublicKey)
require.NoError(t, err)
rev := extensions.Revocation{
Timestamp: time.Now().Unix(),
CertHash: make([]byte, len(leafHash)),
KeyHash: make([]byte, len(leafKeyHash)),
}
copy(rev.CertHash, leafHash)
copy(rev.KeyHash, leafKeyHash[:])
err = rev.Sign(caKey)
assert.NoError(t, err)
assert.NotEmpty(t, rev.Signature)
@ -285,13 +248,14 @@ func TestRevocation_Marshal(t *testing.T) {
assert.NoError(t, err)
leafCert, caKey := chain[0], keys[0]
leafHash := pkcrypto.SHA256Hash(leafCert.Raw)
leafKeyHash, err := peertls.DoubleSHA256PublicKey(leafCert.PublicKey)
require.NoError(t, err)
rev := extensions.Revocation{
Timestamp: time.Now().Unix(),
CertHash: make([]byte, len(leafHash)),
KeyHash: make([]byte, len(leafKeyHash)),
}
copy(rev.CertHash, leafHash)
copy(rev.KeyHash, leafKeyHash[:])
err = rev.Sign(caKey)
assert.NoError(t, err)
assert.NotEmpty(t, rev.Signature)
@ -312,13 +276,14 @@ func TestRevocation_Unmarshal(t *testing.T) {
assert.NoError(t, err)
leafCert, caKey := chain[0], keys[0]
leafHash := pkcrypto.SHA256Hash(leafCert.Raw)
leafKeyHash, err := peertls.DoubleSHA256PublicKey(leafCert.PublicKey)
require.NoError(t, err)
rev := extensions.Revocation{
Timestamp: time.Now().Unix(),
CertHash: make([]byte, len(leafHash)),
KeyHash: make([]byte, len(leafKeyHash)),
}
copy(rev.CertHash, leafHash)
copy(rev.KeyHash, leafKeyHash[:])
err = rev.Sign(caKey)
assert.NoError(t, err)
assert.NotEmpty(t, rev.Signature)

View File

@ -64,7 +64,7 @@ func NewOptions(i *identity.FullIdentity, c Config) (*Options, error) {
func NewExtensionsMap(chain ...*x509.Certificate) ExtensionMap {
extensionMap := make(ExtensionMap)
for _, cert := range chain {
for _, ext := range cert.ExtraExtensions {
for _, ext := range cert.Extensions {
extensionMap[ext.Id.String()] = ext
}
}

View File

@ -4,15 +4,23 @@
package tlsopts_test
import (
"crypto/x509"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"storj.io/storj/internal/testcontext"
"storj.io/storj/internal/testidentity"
"storj.io/storj/internal/testpeertls"
"storj.io/storj/internal/testplanet"
"storj.io/storj/pkg/identity"
"storj.io/storj/pkg/peertls"
"storj.io/storj/pkg/peertls/extensions"
"storj.io/storj/pkg/peertls/tlsopts"
"storj.io/storj/pkg/storj"
"storj.io/storj/storage"
)
func TestVerifyIdentity_success(t *testing.T) {
@ -58,3 +66,90 @@ func TestVerifyIdentity_error(t *testing.T) {
})
}
}
func TestExtensionMap_HandleExtensions(t *testing.T) {
ctx := testcontext.New(t)
defer ctx.Cleanup()
keys, originalChain, err := testpeertls.NewCertChain(2)
assert.NoError(t, err)
rev := new(extensions.Revocation)
// TODO: `keys[peertls.CAIndex]`
oldRevokedLeafChain, revocationExt, err := testpeertls.RevokeLeaf(keys[0], originalChain)
require.NoError(t, err)
err = rev.Unmarshal(revocationExt.Value)
require.NoError(t, err)
err = rev.Verify(oldRevokedLeafChain[peertls.CAIndex])
require.NoError(t, err)
// NB: node ID is the same, timestamp must change
// (see: identity.RevocationDB#Put)
time.Sleep(1 * time.Second)
// TODO: `keys[peertls.CAIndex]`
newRevokedLeafChain, revocationExt, err := testpeertls.RevokeLeaf(keys[0], oldRevokedLeafChain)
require.NoError(t, err)
err = rev.Unmarshal(revocationExt.Value)
require.NoError(t, err)
err = rev.Verify(newRevokedLeafChain[peertls.CAIndex])
require.NoError(t, err)
testidentity.RevocationDBsTest(t, func(t *testing.T, revDB extensions.RevocationDB, db storage.KeyValueStore) {
opts := &extensions.Options{
RevDB: revDB,
}
testcases := []struct {
name string
chain []*x509.Certificate
}{
{"no extensions", originalChain},
{"leaf revocation", oldRevokedLeafChain},
{"double leaf revocation", newRevokedLeafChain},
// TODO: more and more diverse extensions in cases
}
{
handlerFuncMap := extensions.AllHandlers.WithOptions(opts)
for _, testcase := range testcases {
t.Log(testcase.name)
extensionsMap := tlsopts.NewExtensionsMap(testcase.chain...)
err := extensionsMap.HandleExtensions(handlerFuncMap, identity.ToChains(testcase.chain))
assert.NoError(t, err)
}
}
})
}
func TestExtensionMap_HandleExtensions_error(t *testing.T) {
ctx := testcontext.New(t)
defer ctx.Cleanup()
testidentity.RevocationDBsTest(t, func(t *testing.T, revDB extensions.RevocationDB, db storage.KeyValueStore) {
keys, chain, oldRevocation, err := testpeertls.NewRevokedLeafChain()
assert.NoError(t, err)
// NB: node ID is the same, timestamp must change
// (see: identity.RevocationDB#Put)
time.Sleep(time.Second)
_, newRevocation, err := testpeertls.RevokeLeaf(keys[0], chain)
require.NoError(t, err)
assert.NotEqual(t, oldRevocation, newRevocation)
err = revDB.Put(chain, newRevocation)
assert.NoError(t, err)
opts := &extensions.Options{RevDB: revDB}
handlerFuncMap := extensions.HandlerFactories{
extensions.RevocationUpdateHandler,
}.WithOptions(opts)
extensionsMap := tlsopts.NewExtensionsMap(chain[peertls.LeafIndex])
assert.Equal(t, oldRevocation, extensionsMap[extensions.RevocationExtID.String()])
err = extensionsMap.HandleExtensions(handlerFuncMap, identity.ToChains(chain))
assert.Errorf(t, err, extensions.ErrRevocationTimestamp.Error())
})
}

View File

@ -12,7 +12,9 @@ package peertls
// (see https://en.wikipedia.org/wiki/Privacy-enhanced_Electronic_Mail)
import (
"crypto"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"math/big"
@ -35,6 +37,18 @@ func NewNonTemporaryError(err error) NonTemporaryError {
}
}
// DoubleSHA256PublicKey returns the hash of the hash of (double-hash, SHA226)
// the binary format of the given public key.
func DoubleSHA256PublicKey(k crypto.PublicKey) ([sha256.Size]byte, error) {
kb, err := x509.MarshalPKIXPublicKey(k)
if err != nil {
return [sha256.Size]byte{}, err
}
mid := sha256.Sum256(kb)
end := sha256.Sum256(mid[:])
return end, nil
}
// Temporary returns false to indicate that is is a non-temporary error
func (nte NonTemporaryError) Temporary() bool {
return false

View File

@ -6,7 +6,6 @@ package pkcrypto
import (
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"io"
@ -184,9 +183,6 @@ func CertsFromPEM(pemBytes []byte) ([]*x509.Certificate, error) {
switch pemBlock.Type {
case BlockLabelCertificate:
encChain.AddCert(pemBlock.Bytes)
case BlockLabelExtension:
err := encChain.AddExtension(pemBlock.Bytes)
blockErrs.Add(err)
}
}
if err := blockErrs.Err(); err != nil {
@ -197,24 +193,11 @@ func CertsFromPEM(pemBytes []byte) ([]*x509.Certificate, error) {
}
type encodedChain struct {
chain [][]byte
extensions [][][]byte
chain [][]byte
}
func (e *encodedChain) AddCert(b []byte) {
e.chain = append(e.chain, b)
e.extensions = append(e.extensions, [][]byte{})
}
func (e *encodedChain) AddExtension(b []byte) error {
chainLen := len(e.chain)
if chainLen < 1 {
return ErrChainLength.New("expected: >= 1; actual: %d", chainLen)
}
i := chainLen - 1
e.extensions[i] = append(e.extensions[i], b)
return nil
}
func (e *encodedChain) Parse() ([]*x509.Certificate, error) {
@ -223,72 +206,9 @@ func (e *encodedChain) Parse() ([]*x509.Certificate, error) {
return nil, err
}
var extErrs errs.Group
for i, cert := range chain {
for _, ee := range e.extensions[i] {
ext, err := PKIXExtensionFromASN1(ee)
extErrs.Add(err) // TODO: is this correct?
cert.ExtraExtensions = append(cert.ExtraExtensions, *ext)
}
}
if err := extErrs.Err(); err != nil {
return nil, err
}
return chain, nil
}
// WritePKIXExtensionPEM writes the certificate extension to the writer, in a PEM-
// enveloped PKIX form.
func WritePKIXExtensionPEM(w io.Writer, extension *pkix.Extension) error {
extBytes, err := PKIXExtensionToASN1(extension)
if err != nil {
return errs.Wrap(err)
}
err = pem.Encode(w, &pem.Block{Type: BlockLabelExtension, Bytes: extBytes})
return errs.Wrap(err)
}
// PKIXExtensionToPEM serializes a PKIX certificate extension to PEM-
// enveloped ASN.1 bytes.
func PKIXExtensionToPEM(extension *pkix.Extension) ([]byte, error) {
asn, err := PKIXExtensionToASN1(extension)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(&pem.Block{Type: BlockLabelExtension, Bytes: asn}), nil
}
// PKIXExtensionToASN1 serializes a PKIX certificate extension to the
// appropriate ASN.1 structure for such things. See RFC 5280, section 4.1.1.2.
func PKIXExtensionToASN1(extension *pkix.Extension) ([]byte, error) {
extBytes, err := asn1.Marshal(extension)
return extBytes, errs.Wrap(err)
}
// PKIXExtensionFromASN1 deserializes a PKIX certificate extension from
// the appropriate ASN.1 structure for such things.
func PKIXExtensionFromASN1(extData []byte) (*pkix.Extension, error) {
var extension pkix.Extension
if _, err := asn1.Unmarshal(extData, &extension); err != nil {
return nil, ErrParse.New("unable to unmarshal PKIX extension: %v", err)
}
return &extension, nil
}
// PKIXExtensionFromPEM parses a PKIX certificate extension from
// PEM-enveloped ASN.1 bytes.
func PKIXExtensionFromPEM(pemBytes []byte) (*pkix.Extension, error) {
pb, _ := pem.Decode(pemBytes)
if pb == nil {
return nil, ErrParse.New("unable to parse PEM block")
}
if pb.Type != BlockLabelExtension {
return nil, ErrParse.New("can not parse PKIX cert extension from PEM block labeled %q", pb.Type)
}
return PKIXExtensionFromASN1(pb.Bytes)
}
type ecdsaSignature struct {
R, S *big.Int
}