Skip to content
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

Implement NewJwtSigner and CreateAttestation #579

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"fmt"
"github.com/pkg/errors"
"math/big"
)
Expand All @@ -46,6 +47,26 @@ func hashPayload(payload []byte, signingAlg SignatureAlgorithm) (crypto.Hash, []
}
}

func createDetachedSignature(privateKey interface{}, payload []byte, alg SignatureAlgorithm) ([]byte, error) {
switch alg {
case RsaSignPkcs12048Sha256, RsaSignPkcs13072Sha256, RsaSignPkcs14096Sha256, RsaSignPkcs14096Sha512, RsaPss2048Sha256, RsaPss3072Sha256, RsaPss4096Sha256, RsaPss4096Sha512:
rsaKey, ok := privateKey.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("expected rsa key")
}
return rsaSign(rsaKey, payload, alg)
case EcdsaP256Sha256, EcdsaP384Sha384, EcdsaP521Sha512:
ecKey, ok := privateKey.(*ecdsa.PrivateKey)
if !ok {
return nil, errors.New("expected ecdsa key")
}
return ecSign(ecKey, payload, alg)
default:
return nil, fmt.Errorf("unknown signature algorithm: %v", alg)

}
}

// This function will be used to verify PKIX and JWT signatures. PGP detached signatures are not supported by this function.
// Signature is the raw byte signature.
// PublicKey is the PEM encoded public key that will be used to verify the signature.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,58 @@ func TestVerifyDetached(t *testing.T) {
})
}
}

func TestCreateDetachedSignature(t *testing.T) {
tcs := []struct {
name string
key []byte
alg SignatureAlgorithm
expectedError bool
}{
{
name: "create rsa signature success",
key: []byte(rsa2048PrivateKey),
alg: RsaSignPkcs12048Sha256,
expectedError: false,
}, {
name: "create ecdsa signature success",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add two bad cases where alg does not match the key passed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

key: []byte(ec256PrivateKey),
alg: EcdsaP256Sha256,
expectedError: false,
},
{
name: "rsa key with ecdsa alg",
key: []byte(rsa2048PrivateKey),
alg: EcdsaP256Sha256,
expectedError: true,
}, {
name: "ecdsa key with rsa alg",
key: []byte(ec256PrivateKey),
alg: RsaSignPkcs12048Sha256,
expectedError: true,
}, {
name: "unknown singature algorithm",
key: []byte(rsa2048PrivateKey),
alg: UnknownSigningAlgorithm,
expectedError: true,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
privKey, err := parsePkixPrivateKeyPem(tc.key)
if err != nil {
t.Fatalf("failed to parse private key %v", err)
}
_, err = createDetachedSignature(privKey, []byte(payload), tc.alg)
if tc.expectedError {
if err == nil {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: spacing around =

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

t.Errorf("createDetachedSignature(...)=nil, expected non-nil")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit confusing, can be understood as signature is nil.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discussed during sync.

}
} else {
if err != nil {
t.Errorf("createDetachedSignature(...)=%v, expected nil", err)
}
}
})
}
}
60 changes: 60 additions & 0 deletions pkg/attestlib/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,66 @@ func getAlgName(alg SignatureAlgorithm) string {
}
}

type jwtSigner struct {
privateKey interface{}
publicKeyID string
signatureAlgorithm SignatureAlgorithm
}

// NewJwtSigner creates a Signer interface for JWT Attestations. `privateKey`
// contains the PEM-encoded private key. `publicKeyID` is the ID of the public
// key that can verify the Attestation signature. In most cases, publicKeyID should be left empty and will be generated automatically.
func NewJwtSigner(privateKey []byte, alg SignatureAlgorithm, publicKeyID string) (Signer, error) {
key, err := parsePkixPrivateKeyPem(privateKey)
if err != nil {
return nil, errors.Wrap(err, "error parsing private key")
}

// If no ID is provided one is computed based on the default digest-based URI extracted from the public key material
if len(publicKeyID) == 0 {
publicKeyID, err = generatePkixPublicKeyId(key)
if err != nil {
return nil, errors.Wrap(err, "error generating public key id")
}
}
return &jwtSigner{
privateKey: key,
publicKeyID: publicKeyID,
signatureAlgorithm: alg,
}, nil
}

// CreateAttestation creates a signed JWT Attestation. See Signer for more details.
// jsonJwtBody is the second section in the JWT. This should contain the following claims:
// "sub" = container:digest:sha256:<my-image-digest>, "aud" : "//binaryauthorization.googleapis.com", "attestationType" : "claimless"
func (s *jwtSigner) CreateAttestation(jsonJwtBody []byte) (*Attestation, error) {
type headerTemplate struct {
typ, alg, kid string
}
header := headerTemplate{
typ: "JWT",
alg: getAlgName(s.signatureAlgorithm),
kid: s.publicKeyID,
}

headerJson, err := json.Marshal(header)
if err != nil {
return nil, errors.Wrap(err, "error marshaling header")
}
headerBase64 := base64.RawURLEncoding.EncodeToString(headerJson)
jsonJwtBodyBase64 := base64.RawURLEncoding.EncodeToString(jsonJwtBody)
signature, err := createDetachedSignature(s.privateKey, []byte(headerBase64+"."+jsonJwtBodyBase64), s.signatureAlgorithm)
if err != nil {
return nil, errors.Wrap(err, "error creating signature")
}
signatureBase64 := base64.RawURLEncoding.EncodeToString(signature)
jwt := headerBase64 + "." + jsonJwtBodyBase64 + "." + signatureBase64
return &Attestation{
PublicKeyID: s.publicKeyID,
Signature: []byte(jwt),
}, nil
}

func checkHeader(headerIn []byte, publicKey PublicKey) error {
type headerTemplate struct {
Typ, Alg, Kid, Crit string
Expand Down
61 changes: 60 additions & 1 deletion pkg/attestlib/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,69 @@ func TestVerifyJWT(t *testing.T) {
}
} else {
if err != nil {
t.Errorf("Unexpected error: %e", err)
t.Errorf("Unexpected error: %v", err)
}

}
})
}
}

func TestNewJwtSigner(t *testing.T) {
tcs := []struct {
name string
key []byte
publicKeyId string
alg SignatureAlgorithm
expectedError bool
}{
{
name: "new jwt signer success",
key: []byte(rsa2048PrivateKey),
publicKeyId: "kid",
alg: RsaSignPkcs12048Sha256,
expectedError: false,
},
{
name: "new jwt signer with no key id success",
key: []byte(rsa2048PrivateKey),
publicKeyId: "",
alg: RsaSignPkcs12048Sha256,
expectedError: false,
},
{
name: "new jwt signer with bad key fails",
key: []byte("some-key"),
publicKeyId: "",
alg: RsaSignPkcs12048Sha256,
expectedError: true,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
_, err := NewJwtSigner(tc.key, tc.alg, tc.publicKeyId)
if tc.expectedError {
if err == nil {
t.Errorf("NewJwtSigner(...)=nil, expected non nil")
}
} else {
if err != nil {
t.Errorf("NewJwtSigner(...)=%v, expected nil", err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: spacing around =

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}
}
})
}
}

func TestCreateJwtAttestation(t *testing.T) {
signer, err := NewJwtSigner([]byte(rsa2048PrivateKey), RsaSignPkcs12048Sha256, "kid")
if err != nil {
t.Fatalf("failed to create signer")
}
attestation, err := signer.CreateAttestation([]byte(payload))
if err != nil {
t.Errorf("CreateAttestation(..)=%v, expected nil", err)
} else if attestation.PublicKeyID != "kid" {
t.Errorf("attestation.PublicKeyID=%v, expected kid", attestation.PublicKeyID)
}
}
43 changes: 8 additions & 35 deletions pkg/attestlib/pkix.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ limitations under the License.
package attestlib

import (
"crypto/ecdsa"
"crypto/rsa"
"fmt"
"github.com/pkg/errors"
)

Expand Down Expand Up @@ -54,39 +51,15 @@ func NewPkixSigner(privateKey []byte, alg SignatureAlgorithm, publicKeyID string

// CreateAttestation creates a signed PKIX Attestation. See Signer for more details.
func (s *pkixSigner) CreateAttestation(payload []byte) (*Attestation, error) {
switch s.signatureAlgorithm {
case RsaSignPkcs12048Sha256, RsaSignPkcs13072Sha256, RsaSignPkcs14096Sha256, RsaSignPkcs14096Sha512, RsaPss2048Sha256, RsaPss3072Sha256, RsaPss4096Sha256, RsaPss4096Sha512:
rsaKey, ok := s.privateKey.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("expected rsa key")
}
signature, err := rsaSign(rsaKey, payload, s.signatureAlgorithm)
if err != nil {
return nil, errors.Wrap(err, "error creating rsa signature")
}
return &Attestation{
PublicKeyID: s.publicKeyID,
Signature: signature,
SerializedPayload: payload,
}, nil
case EcdsaP256Sha256, EcdsaP384Sha384, EcdsaP521Sha512:
ecKey, ok := s.privateKey.(*ecdsa.PrivateKey)
if !ok {
return nil, errors.New("expected ecdsa key")
}
signature, err := ecSign(ecKey, payload, s.signatureAlgorithm)
if err != nil {
return nil, errors.Wrap(err, "error creating ecdsa signature")
}
return &Attestation{
PublicKeyID: s.publicKeyID,
Signature: signature,
SerializedPayload: payload,
}, nil
default:
return nil, fmt.Errorf("unknown signature algorithm: %v", s.signatureAlgorithm)

signature, err := createDetachedSignature(s.privateKey, payload, s.signatureAlgorithm)
if err != nil {
return nil, errors.Wrap(err, "error creating signature")
}
return &Attestation{
PublicKeyID: s.publicKeyID,
Signature: signature,
SerializedPayload: payload,
}, nil
}

type pkixVerifierImpl struct{}
Expand Down
24 changes: 0 additions & 24 deletions pkg/attestlib/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ limitations under the License.

package attestlib

import "errors"

// Signer contains methods to create a signed Attestation.
type Signer interface {
// CreateAttestation creates an Attestation whose signature is generated by
Expand All @@ -26,25 +24,3 @@ type Signer interface {
// but unsigned token.
CreateAttestation(payload []byte) (*Attestation, error)
}

type jwtSigner struct {
PrivateKey []byte
PublicKeyID string
SignatureAlgorithm SignatureAlgorithm
}

// NewJwtSigner creates a Signer interface for JWT Attestations. `publicKeyID`
// is the ID of the public key that can verify the Attestation signature.
// TODO: Explain formatting of JWT private keys.
func NewJwtSigner(privateKey []byte, publicKeyID string, alg SignatureAlgorithm) (Signer, error) {
return &jwtSigner{
PrivateKey: privateKey,
PublicKeyID: publicKeyID,
SignatureAlgorithm: alg,
}, nil
}

// CreateAttestation creates a signed JWT Attestation. See Signer for more details.
func (s *jwtSigner) CreateAttestation(payload []byte) (*Attestation, error) {
return nil, errors.New("jwt attestations not implemented")
}