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,47 @@ 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: "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)
}
}
})
}
}
59 changes: 59 additions & 0 deletions pkg/attestlib/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,65 @@ func getAlgName(alg SignatureAlgorithm) string {
}
}

type jwtSigner struct {
privateKey interface{}
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.

Choose a reason for hiding this comment

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

Move "publicKeyID" to be the last arguement, and in comment say that it should normally be left empty.

Alternatively, I like the idea of having two functions "NewJwtSigner" and "NewJwtSignerExplcitKeyId" , and have the former generate kid and call the latter.

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.

// TODO: Explain formatting of JWT private keys.

Choose a reason for hiding this comment

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

name or bug/github issue with all TODOs.

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 NewJwtSigner(privateKey []byte, publicKeyID string, alg SignatureAlgorithm) (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.
func (s *jwtSigner) CreateAttestation(payload []byte) (*Attestation, error) {

Choose a reason for hiding this comment

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

payload -> JsonJwtBody, with comment explaining what that is.

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.

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)
payloadBase64 := base64.RawURLEncoding.EncodeToString(payload)
signature, err := createDetachedSignature(s.privateKey, []byte(headerBase64+"."+payloadBase64), s.signatureAlgorithm)
if err != nil {
return nil, errors.Wrap(err, "error creating signature")
}
signatureBase64 := base64.RawURLEncoding.EncodeToString(signature)
jwt := headerBase64 + "." + payloadBase64 + "." + signatureBase64
return &Attestation{
PublicKeyID: s.publicKeyID,
Signature: []byte(jwt),
SerializedPayload: payload,

Choose a reason for hiding this comment

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

from attestation.go: // SerializedPayload stores the payload over which the signature was
// signed. This field is only used for PKIX Attestations.

This is not accurate in the case of JWTs as written now. I think the cleanest way to handle it is to leave SerializedPayload empty for JWTs and update the documentation to reflect that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this was a mistake. I updated it to leave the SerializedPayload field empty.

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.

}, 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 singer success",
Copy link
Contributor

Choose a reason for hiding this comment

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

typo: singer->signer

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(rsa2048PrivateKey),
publicKeyId: "kid",
alg: RsaSignPkcs12048Sha256,
expectedError: false,
},
{
name: "new jwt singer with no key id success",
Copy link
Contributor

Choose a reason for hiding this comment

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

same typo

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(rsa2048PrivateKey),
publicKeyId: "",
alg: RsaSignPkcs12048Sha256,
expectedError: false,
},
{
name: "new jwt singer 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.publicKeyId, tc.alg)
if tc.expectedError {
if err == nil {
t.Errorf("NewJwtSigner(...) = 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.

nit: spacing around =

Also ditto as above, would this be confusing as to which return value it is referring to?

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.

}
} 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), "kid", RsaSignPkcs12048Sha256)
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)
Copy link
Contributor

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.

} else if attestation.PublicKeyID != "kid" {
t.Errorf("attestation.PublicKeyID = %v, expected kid", attestation.PublicKeyID)
Copy link
Contributor

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.

}
}
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,37 +51,13 @@ 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
}
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")
}