diff --git a/jwk.go b/jwk.go index ccdfb2e8..59d9a924 100644 --- a/jwk.go +++ b/jwk.go @@ -21,6 +21,8 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rsa" + "crypto/x509" + "encoding/base64" "fmt" "math/big" "reflect" @@ -49,14 +51,17 @@ type rawJsonWebKey struct { Dp *byteBuffer `json:"dp,omitempty"` Dq *byteBuffer `json:"dq,omitempty"` Qi *byteBuffer `json:"qi,omitempty"` + // Certificates + X5c []string `json:"x5c,omitempty"` } // JsonWebKey represents a public or private key in JWK format. type JsonWebKey struct { - Key interface{} - KeyID string - Algorithm string - Use string + Key interface{} + Certificates []*x509.Certificate + KeyID string + Algorithm string + Use string } // MarshalJSON serializes the given key to its JSON representation. @@ -87,6 +92,10 @@ func (k JsonWebKey) MarshalJSON() ([]byte, error) { raw.Alg = k.Algorithm raw.Use = k.Use + for _, cert := range k.Certificates { + raw.X5c = append(raw.X5c, base64.StdEncoding.EncodeToString(cert.Raw)) + } + return MarshalJSON(raw) } @@ -121,6 +130,19 @@ func (k *JsonWebKey) UnmarshalJSON(data []byte) (err error) { if err == nil { *k = JsonWebKey{Key: key, KeyID: raw.Kid, Algorithm: raw.Alg, Use: raw.Use} } + + k.Certificates = make([]*x509.Certificate, len(raw.X5c)) + for i, cert := range raw.X5c { + raw, err := base64.StdEncoding.DecodeString(cert) + if err != nil { + return err + } + k.Certificates[i], err = x509.ParseCertificate(raw) + if err != nil { + return err + } + } + return } diff --git a/jwk_test.go b/jwk_test.go index a87b42a1..9ea7b451 100644 --- a/jwk_test.go +++ b/jwk_test.go @@ -22,6 +22,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rsa" + "crypto/x509" "encoding/hex" "fmt" "math/big" @@ -29,6 +30,48 @@ import ( "testing" ) +// Test chain of two X.509 certificates +var testCertificates, _ = x509.ParseCertificates(fromBase64Bytes(` +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`)) + func TestCurveSize(t *testing.T) { size256 := curveSize(elliptic.P256()) size384 := curveSize(elliptic.P384()) @@ -156,6 +199,38 @@ func TestRoundtripEcPrivate(t *testing.T) { } } +func TestRoundtripX5C(t *testing.T) { + jwk := JsonWebKey{ + Key: rsaTestKey, + KeyID: "bar", + Algorithm: "foo", + Certificates: testCertificates, + } + + jsonbar, err := jwk.MarshalJSON() + if err != nil { + t.Error("problem marshaling", err) + } + + var jwk2 JsonWebKey + err = jwk2.UnmarshalJSON(jsonbar) + if err != nil { + t.Error("problem unmarshalling", err) + } + + if !reflect.DeepEqual(testCertificates, jwk2.Certificates) { + t.Error("Certificates not equal", jwk.Certificates, jwk2.Certificates) + } + + jsonbar2, err := jwk2.MarshalJSON() + if err != nil { + t.Error("problem marshaling", err) + } + if !bytes.Equal(jsonbar, jsonbar2) { + t.Error("roundtrip should not lose information") + } +} + func TestMarshalUnmarshal(t *testing.T) { kid := "DEADBEEF" @@ -383,6 +458,38 @@ var cookbookJWKs = []string{ "qi":"lSQi-w9CpyUReMErP1RsBLk7wNtOvs5EQpPqmuMvqW57NBUczScEoPwmUqq abu9V0-Py4dQ57_bapoKRu1R90bvuFnU63SHWEFglZQvJDMeAvmj4sm-Fp0o Yu_neotgQ0hzbI5gry7ajdYy9-2lNx_76aBZoOUu9HCJ-UsfSOI8"}`), + + // X.509 Certificate Chain + stripWhitespace(`{"kty":"RSA", + "use":"sig", + "kid":"1b94c", + "n":"vrjOfz9Ccdgx5nQudyhdoR17V-IubWMeOZCwX_jj0hgAsz2J_pqYW08 + PLbK_PdiVGKPrqzmDIsLI7sA25VEnHU1uCLNwBuUiCO11_-7dYbsr4iJmG0Q + u2j8DsVyT1azpJC_NG84Ty5KKthuCaPod7iI7w0LK9orSMhBEwwZDCxTWq4a + YWAchc8t-emd9qOvWtVMDC2BXksRngh6X5bUYLy6AyHKvj-nUy1wgzjYQDwH + MTplCoLtU-o-8SNnZ1tmRoGE9uJkBLdh5gFENabWnU5m1ZqZPdwS-qo-meMv + VfJb6jJVWRpl2SUtCnYG2C32qvbWbjZ_jBPD5eunqsIo1vQ", + "e":"AQAB", + "x5c": + ["MIIDQjCCAiqgAwIBAgIGATz/FuLiMA0GCSqGSIb3DQEBBQUAMGIxCzAJB + gNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRwwGgYD + VQQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5CcmlhbiBDYW1 + wYmVsbDAeFw0xMzAyMjEyMzI5MTVaFw0xODA4MTQyMjI5MTVaMGIxCzAJBg + NVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRwwGgYDV + QQKExNQaW5nIElkZW50aXR5IENvcnAuMRcwFQYDVQQDEw5CcmlhbiBDYW1w + YmVsbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL64zn8/QnH + YMeZ0LncoXaEde1fiLm1jHjmQsF/449IYALM9if6amFtPDy2yvz3YlRij66 + s5gyLCyO7ANuVRJx1NbgizcAblIgjtdf/u3WG7K+IiZhtELto/A7Fck9Ws6 + SQvzRvOE8uSirYbgmj6He4iO8NCyvaK0jIQRMMGQwsU1quGmFgHIXPLfnpn + fajr1rVTAwtgV5LEZ4Iel+W1GC8ugMhyr4/p1MtcIM42EA8BzE6ZQqC7VPq + PvEjZ2dbZkaBhPbiZAS3YeYBRDWm1p1OZtWamT3cEvqqPpnjL1XyW+oyVVk + aZdklLQp2Btgt9qr21m42f4wTw+Xrp6rCKNb0CAwEAATANBgkqhkiG9w0BA + QUFAAOCAQEAh8zGlfSlcI0o3rYDPBB07aXNswb4ECNIKG0CETTUxmXl9KUL + +9gGlqCz5iWLOgWsnrcKcY0vXPG9J1r9AqBNTqNgHq2G03X09266X5CpOe1 + zFo+Owb1zxtp3PehFdfQJ610CDLEaS9V9Rqp17hCyybEpOGVwe8fnk+fbEL + 2Bo3UPGrpsHzUoaGpDftmWssZkhpBJKVMJyf/RuP2SmmaIzmnw9JiSlYhzo + 4tpzd5rFXhjRbg4zW9C+2qok+2+qDM1iJ684gPHMIY8aLWrdgQTxkumGmTq + gawR+N5MDtdPTEQ0XfIBc2cJEUyMTY5MPvACWpkA6SdS4xSvdXK3IVfOWA=="]}`), } // SHA-256 thumbprints of the above keys, hex-encoded @@ -391,6 +498,7 @@ var cookbookJWKThumbprints = []string{ "747ae2dd2003664aeeb21e4753fe7402846170a16bc8df8f23a8cf06d3cbe793", "f63838e96077ad1fc01c3f8405774dedc0641f558ebb4b40dccf5f9b6d66a932", "0fc478f8579325fcee0d4cbc6d9d1ce21730a6e97e435d6008fb379b0ebe47d4", + "0ddb05bfedbec2070fa037324ba397396561d3425d6d69245570c261dc49dee3", } func TestWebKeyVectorsValid(t *testing.T) {