diff --git a/pkg/attestlib/verify_detached.go b/pkg/attestlib/detached_signature.go similarity index 83% rename from pkg/attestlib/verify_detached.go rename to pkg/attestlib/detached_signature.go index bff785eb2..0995856aa 100644 --- a/pkg/attestlib/verify_detached.go +++ b/pkg/attestlib/detached_signature.go @@ -25,6 +25,7 @@ import ( "crypto/x509" "encoding/asn1" "encoding/pem" + "fmt" "github.com/pkg/errors" "math/big" ) @@ -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. diff --git a/pkg/attestlib/verify_detached_test.go b/pkg/attestlib/detached_signature_test.go similarity index 90% rename from pkg/attestlib/verify_detached_test.go rename to pkg/attestlib/detached_signature_test.go index 222135bf2..2b9811741 100644 --- a/pkg/attestlib/verify_detached_test.go +++ b/pkg/attestlib/detached_signature_test.go @@ -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", + 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 { + t.Errorf("createDetachedSignature(...)=nil, expected non-nil") + } + } else { + if err != nil { + t.Errorf("createDetachedSignature(...)=%v, expected nil", err) + } + } + }) + } +} diff --git a/pkg/attestlib/jwt.go b/pkg/attestlib/jwt.go index 9ba5a1d19..04d9cf125 100644 --- a/pkg/attestlib/jwt.go +++ b/pkg/attestlib/jwt.go @@ -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:, "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 diff --git a/pkg/attestlib/jwt_test.go b/pkg/attestlib/jwt_test.go index c5cd5ecff..d32979859 100644 --- a/pkg/attestlib/jwt_test.go +++ b/pkg/attestlib/jwt_test.go @@ -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) + } + } + }) + } +} + +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) + } +} diff --git a/pkg/attestlib/pkix.go b/pkg/attestlib/pkix.go index 3aefdfa8c..ec0e2808c 100644 --- a/pkg/attestlib/pkix.go +++ b/pkg/attestlib/pkix.go @@ -17,9 +17,6 @@ limitations under the License. package attestlib import ( - "crypto/ecdsa" - "crypto/rsa" - "fmt" "github.com/pkg/errors" ) @@ -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{} diff --git a/pkg/attestlib/signer.go b/pkg/attestlib/signer.go index bd582693a..498362a4c 100644 --- a/pkg/attestlib/signer.go +++ b/pkg/attestlib/signer.go @@ -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 @@ -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") -}