| package libtrust |
| |
| import ( |
| "bytes" |
| "crypto" |
| "crypto/x509" |
| "encoding/base64" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "sort" |
| "time" |
| "unicode" |
| ) |
| |
| var ( |
| // ErrInvalidSignContent is used when the content to be signed is invalid. |
| ErrInvalidSignContent = errors.New("invalid sign content") |
| |
| // ErrInvalidJSONContent is used when invalid json is encountered. |
| ErrInvalidJSONContent = errors.New("invalid json content") |
| |
| // ErrMissingSignatureKey is used when the specified signature key |
| // does not exist in the JSON content. |
| ErrMissingSignatureKey = errors.New("missing signature key") |
| ) |
| |
| type jsHeader struct { |
| JWK PublicKey `json:"jwk,omitempty"` |
| Algorithm string `json:"alg"` |
| Chain []string `json:"x5c,omitempty"` |
| } |
| |
| type jsSignature struct { |
| Header jsHeader `json:"header"` |
| Signature string `json:"signature"` |
| Protected string `json:"protected,omitempty"` |
| } |
| |
| type jsSignaturesSorted []jsSignature |
| |
| func (jsbkid jsSignaturesSorted) Swap(i, j int) { jsbkid[i], jsbkid[j] = jsbkid[j], jsbkid[i] } |
| func (jsbkid jsSignaturesSorted) Len() int { return len(jsbkid) } |
| |
| func (jsbkid jsSignaturesSorted) Less(i, j int) bool { |
| ki, kj := jsbkid[i].Header.JWK.KeyID(), jsbkid[j].Header.JWK.KeyID() |
| si, sj := jsbkid[i].Signature, jsbkid[j].Signature |
| |
| if ki == kj { |
| return si < sj |
| } |
| |
| return ki < kj |
| } |
| |
| type signKey struct { |
| PrivateKey |
| Chain []*x509.Certificate |
| } |
| |
| // JSONSignature represents a signature of a json object. |
| type JSONSignature struct { |
| payload string |
| signatures []jsSignature |
| indent string |
| formatLength int |
| formatTail []byte |
| } |
| |
| func newJSONSignature() *JSONSignature { |
| return &JSONSignature{ |
| signatures: make([]jsSignature, 0, 1), |
| } |
| } |
| |
| // Payload returns the encoded payload of the signature. This |
| // payload should not be signed directly |
| func (js *JSONSignature) Payload() ([]byte, error) { |
| return joseBase64UrlDecode(js.payload) |
| } |
| |
| func (js *JSONSignature) protectedHeader() (string, error) { |
| protected := map[string]interface{}{ |
| "formatLength": js.formatLength, |
| "formatTail": joseBase64UrlEncode(js.formatTail), |
| "time": time.Now().UTC().Format(time.RFC3339), |
| } |
| protectedBytes, err := json.Marshal(protected) |
| if err != nil { |
| return "", err |
| } |
| |
| return joseBase64UrlEncode(protectedBytes), nil |
| } |
| |
| func (js *JSONSignature) signBytes(protectedHeader string) ([]byte, error) { |
| buf := make([]byte, len(js.payload)+len(protectedHeader)+1) |
| copy(buf, protectedHeader) |
| buf[len(protectedHeader)] = '.' |
| copy(buf[len(protectedHeader)+1:], js.payload) |
| return buf, nil |
| } |
| |
| // Sign adds a signature using the given private key. |
| func (js *JSONSignature) Sign(key PrivateKey) error { |
| protected, err := js.protectedHeader() |
| if err != nil { |
| return err |
| } |
| signBytes, err := js.signBytes(protected) |
| if err != nil { |
| return err |
| } |
| sigBytes, algorithm, err := key.Sign(bytes.NewReader(signBytes), crypto.SHA256) |
| if err != nil { |
| return err |
| } |
| |
| js.signatures = append(js.signatures, jsSignature{ |
| Header: jsHeader{ |
| JWK: key.PublicKey(), |
| Algorithm: algorithm, |
| }, |
| Signature: joseBase64UrlEncode(sigBytes), |
| Protected: protected, |
| }) |
| |
| return nil |
| } |
| |
| // SignWithChain adds a signature using the given private key |
| // and setting the x509 chain. The public key of the first element |
| // in the chain must be the public key corresponding with the sign key. |
| func (js *JSONSignature) SignWithChain(key PrivateKey, chain []*x509.Certificate) error { |
| // Ensure key.Chain[0] is public key for key |
| //key.Chain.PublicKey |
| //key.PublicKey().CryptoPublicKey() |
| |
| // Verify chain |
| protected, err := js.protectedHeader() |
| if err != nil { |
| return err |
| } |
| signBytes, err := js.signBytes(protected) |
| if err != nil { |
| return err |
| } |
| sigBytes, algorithm, err := key.Sign(bytes.NewReader(signBytes), crypto.SHA256) |
| if err != nil { |
| return err |
| } |
| |
| header := jsHeader{ |
| Chain: make([]string, len(chain)), |
| Algorithm: algorithm, |
| } |
| |
| for i, cert := range chain { |
| header.Chain[i] = base64.StdEncoding.EncodeToString(cert.Raw) |
| } |
| |
| js.signatures = append(js.signatures, jsSignature{ |
| Header: header, |
| Signature: joseBase64UrlEncode(sigBytes), |
| Protected: protected, |
| }) |
| |
| return nil |
| } |
| |
| // Verify verifies all the signatures and returns the list of |
| // public keys used to sign. Any x509 chains are not checked. |
| func (js *JSONSignature) Verify() ([]PublicKey, error) { |
| keys := make([]PublicKey, len(js.signatures)) |
| for i, signature := range js.signatures { |
| signBytes, err := js.signBytes(signature.Protected) |
| if err != nil { |
| return nil, err |
| } |
| var publicKey PublicKey |
| if len(signature.Header.Chain) > 0 { |
| certBytes, err := base64.StdEncoding.DecodeString(signature.Header.Chain[0]) |
| if err != nil { |
| return nil, err |
| } |
| cert, err := x509.ParseCertificate(certBytes) |
| if err != nil { |
| return nil, err |
| } |
| publicKey, err = FromCryptoPublicKey(cert.PublicKey) |
| if err != nil { |
| return nil, err |
| } |
| } else if signature.Header.JWK != nil { |
| publicKey = signature.Header.JWK |
| } else { |
| return nil, errors.New("missing public key") |
| } |
| |
| sigBytes, err := joseBase64UrlDecode(signature.Signature) |
| if err != nil { |
| return nil, err |
| } |
| |
| err = publicKey.Verify(bytes.NewReader(signBytes), signature.Header.Algorithm, sigBytes) |
| if err != nil { |
| return nil, err |
| } |
| |
| keys[i] = publicKey |
| } |
| return keys, nil |
| } |
| |
| // VerifyChains verifies all the signatures and the chains associated |
| // with each signature and returns the list of verified chains. |
| // Signatures without an x509 chain are not checked. |
| func (js *JSONSignature) VerifyChains(ca *x509.CertPool) ([][]*x509.Certificate, error) { |
| chains := make([][]*x509.Certificate, 0, len(js.signatures)) |
| for _, signature := range js.signatures { |
| signBytes, err := js.signBytes(signature.Protected) |
| if err != nil { |
| return nil, err |
| } |
| var publicKey PublicKey |
| if len(signature.Header.Chain) > 0 { |
| certBytes, err := base64.StdEncoding.DecodeString(signature.Header.Chain[0]) |
| if err != nil { |
| return nil, err |
| } |
| cert, err := x509.ParseCertificate(certBytes) |
| if err != nil { |
| return nil, err |
| } |
| publicKey, err = FromCryptoPublicKey(cert.PublicKey) |
| if err != nil { |
| return nil, err |
| } |
| intermediates := x509.NewCertPool() |
| if len(signature.Header.Chain) > 1 { |
| intermediateChain := signature.Header.Chain[1:] |
| for i := range intermediateChain { |
| certBytes, err := base64.StdEncoding.DecodeString(intermediateChain[i]) |
| if err != nil { |
| return nil, err |
| } |
| intermediate, err := x509.ParseCertificate(certBytes) |
| if err != nil { |
| return nil, err |
| } |
| intermediates.AddCert(intermediate) |
| } |
| } |
| |
| verifyOptions := x509.VerifyOptions{ |
| Intermediates: intermediates, |
| Roots: ca, |
| } |
| |
| verifiedChains, err := cert.Verify(verifyOptions) |
| if err != nil { |
| return nil, err |
| } |
| chains = append(chains, verifiedChains...) |
| |
| sigBytes, err := joseBase64UrlDecode(signature.Signature) |
| if err != nil { |
| return nil, err |
| } |
| |
| err = publicKey.Verify(bytes.NewReader(signBytes), signature.Header.Algorithm, sigBytes) |
| if err != nil { |
| return nil, err |
| } |
| } |
| |
| } |
| return chains, nil |
| } |
| |
| // JWS returns JSON serialized JWS according to |
| // http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-7.2 |
| func (js *JSONSignature) JWS() ([]byte, error) { |
| if len(js.signatures) == 0 { |
| return nil, errors.New("missing signature") |
| } |
| |
| sort.Sort(jsSignaturesSorted(js.signatures)) |
| |
| jsonMap := map[string]interface{}{ |
| "payload": js.payload, |
| "signatures": js.signatures, |
| } |
| |
| return json.MarshalIndent(jsonMap, "", " ") |
| } |
| |
| func notSpace(r rune) bool { |
| return !unicode.IsSpace(r) |
| } |
| |
| func detectJSONIndent(jsonContent []byte) (indent string) { |
| if len(jsonContent) > 2 && jsonContent[0] == '{' && jsonContent[1] == '\n' { |
| quoteIndex := bytes.IndexRune(jsonContent[1:], '"') |
| if quoteIndex > 0 { |
| indent = string(jsonContent[2 : quoteIndex+1]) |
| } |
| } |
| return |
| } |
| |
| type jsParsedHeader struct { |
| JWK json.RawMessage `json:"jwk"` |
| Algorithm string `json:"alg"` |
| Chain []string `json:"x5c"` |
| } |
| |
| type jsParsedSignature struct { |
| Header jsParsedHeader `json:"header"` |
| Signature string `json:"signature"` |
| Protected string `json:"protected"` |
| } |
| |
| // ParseJWS parses a JWS serialized JSON object into a Json Signature. |
| func ParseJWS(content []byte) (*JSONSignature, error) { |
| type jsParsed struct { |
| Payload string `json:"payload"` |
| Signatures []jsParsedSignature `json:"signatures"` |
| } |
| parsed := &jsParsed{} |
| err := json.Unmarshal(content, parsed) |
| if err != nil { |
| return nil, err |
| } |
| if len(parsed.Signatures) == 0 { |
| return nil, errors.New("missing signatures") |
| } |
| payload, err := joseBase64UrlDecode(parsed.Payload) |
| if err != nil { |
| return nil, err |
| } |
| |
| js, err := NewJSONSignature(payload) |
| if err != nil { |
| return nil, err |
| } |
| js.signatures = make([]jsSignature, len(parsed.Signatures)) |
| for i, signature := range parsed.Signatures { |
| header := jsHeader{ |
| Algorithm: signature.Header.Algorithm, |
| } |
| if signature.Header.Chain != nil { |
| header.Chain = signature.Header.Chain |
| } |
| if signature.Header.JWK != nil { |
| publicKey, err := UnmarshalPublicKeyJWK([]byte(signature.Header.JWK)) |
| if err != nil { |
| return nil, err |
| } |
| header.JWK = publicKey |
| } |
| js.signatures[i] = jsSignature{ |
| Header: header, |
| Signature: signature.Signature, |
| Protected: signature.Protected, |
| } |
| } |
| |
| return js, nil |
| } |
| |
| // NewJSONSignature returns a new unsigned JWS from a json byte array. |
| // JSONSignature will need to be signed before serializing or storing. |
| // Optionally, one or more signatures can be provided as byte buffers, |
| // containing serialized JWS signatures, to assemble a fully signed JWS |
| // package. It is the callers responsibility to ensure uniqueness of the |
| // provided signatures. |
| func NewJSONSignature(content []byte, signatures ...[]byte) (*JSONSignature, error) { |
| var dataMap map[string]interface{} |
| err := json.Unmarshal(content, &dataMap) |
| if err != nil { |
| return nil, err |
| } |
| |
| js := newJSONSignature() |
| js.indent = detectJSONIndent(content) |
| |
| js.payload = joseBase64UrlEncode(content) |
| |
| // Find trailing } and whitespace, put in protected header |
| closeIndex := bytes.LastIndexFunc(content, notSpace) |
| if content[closeIndex] != '}' { |
| return nil, ErrInvalidJSONContent |
| } |
| lastRuneIndex := bytes.LastIndexFunc(content[:closeIndex], notSpace) |
| if content[lastRuneIndex] == ',' { |
| return nil, ErrInvalidJSONContent |
| } |
| js.formatLength = lastRuneIndex + 1 |
| js.formatTail = content[js.formatLength:] |
| |
| if len(signatures) > 0 { |
| for _, signature := range signatures { |
| var parsedJSig jsParsedSignature |
| |
| if err := json.Unmarshal(signature, &parsedJSig); err != nil { |
| return nil, err |
| } |
| |
| // TODO(stevvooe): A lot of the code below is repeated in |
| // ParseJWS. It will require more refactoring to fix that. |
| jsig := jsSignature{ |
| Header: jsHeader{ |
| Algorithm: parsedJSig.Header.Algorithm, |
| }, |
| Signature: parsedJSig.Signature, |
| Protected: parsedJSig.Protected, |
| } |
| |
| if parsedJSig.Header.Chain != nil { |
| jsig.Header.Chain = parsedJSig.Header.Chain |
| } |
| |
| if parsedJSig.Header.JWK != nil { |
| publicKey, err := UnmarshalPublicKeyJWK([]byte(parsedJSig.Header.JWK)) |
| if err != nil { |
| return nil, err |
| } |
| jsig.Header.JWK = publicKey |
| } |
| |
| js.signatures = append(js.signatures, jsig) |
| } |
| } |
| |
| return js, nil |
| } |
| |
| // NewJSONSignatureFromMap returns a new unsigned JSONSignature from a map or |
| // struct. JWS will need to be signed before serializing or storing. |
| func NewJSONSignatureFromMap(content interface{}) (*JSONSignature, error) { |
| switch content.(type) { |
| case map[string]interface{}: |
| case struct{}: |
| default: |
| return nil, errors.New("invalid data type") |
| } |
| |
| js := newJSONSignature() |
| js.indent = " " |
| |
| payload, err := json.MarshalIndent(content, "", js.indent) |
| if err != nil { |
| return nil, err |
| } |
| js.payload = joseBase64UrlEncode(payload) |
| |
| // Remove '\n}' from formatted section, put in protected header |
| js.formatLength = len(payload) - 2 |
| js.formatTail = payload[js.formatLength:] |
| |
| return js, nil |
| } |
| |
| func readIntFromMap(key string, m map[string]interface{}) (int, bool) { |
| value, ok := m[key] |
| if !ok { |
| return 0, false |
| } |
| switch v := value.(type) { |
| case int: |
| return v, true |
| case float64: |
| return int(v), true |
| default: |
| return 0, false |
| } |
| } |
| |
| func readStringFromMap(key string, m map[string]interface{}) (v string, ok bool) { |
| value, ok := m[key] |
| if !ok { |
| return "", false |
| } |
| v, ok = value.(string) |
| return |
| } |
| |
| // ParsePrettySignature parses a formatted signature into a |
| // JSON signature. If the signatures are missing the format information |
| // an error is thrown. The formatted signature must be created by |
| // the same method as format signature. |
| func ParsePrettySignature(content []byte, signatureKey string) (*JSONSignature, error) { |
| var contentMap map[string]json.RawMessage |
| err := json.Unmarshal(content, &contentMap) |
| if err != nil { |
| return nil, fmt.Errorf("error unmarshalling content: %s", err) |
| } |
| sigMessage, ok := contentMap[signatureKey] |
| if !ok { |
| return nil, ErrMissingSignatureKey |
| } |
| |
| var signatureBlocks []jsParsedSignature |
| err = json.Unmarshal([]byte(sigMessage), &signatureBlocks) |
| if err != nil { |
| return nil, fmt.Errorf("error unmarshalling signatures: %s", err) |
| } |
| |
| js := newJSONSignature() |
| js.signatures = make([]jsSignature, len(signatureBlocks)) |
| |
| for i, signatureBlock := range signatureBlocks { |
| protectedBytes, err := joseBase64UrlDecode(signatureBlock.Protected) |
| if err != nil { |
| return nil, fmt.Errorf("base64 decode error: %s", err) |
| } |
| var protectedHeader map[string]interface{} |
| err = json.Unmarshal(protectedBytes, &protectedHeader) |
| if err != nil { |
| return nil, fmt.Errorf("error unmarshalling protected header: %s", err) |
| } |
| |
| formatLength, ok := readIntFromMap("formatLength", protectedHeader) |
| if !ok { |
| return nil, errors.New("missing formatted length") |
| } |
| encodedTail, ok := readStringFromMap("formatTail", protectedHeader) |
| if !ok { |
| return nil, errors.New("missing formatted tail") |
| } |
| formatTail, err := joseBase64UrlDecode(encodedTail) |
| if err != nil { |
| return nil, fmt.Errorf("base64 decode error on tail: %s", err) |
| } |
| if js.formatLength == 0 { |
| js.formatLength = formatLength |
| } else if js.formatLength != formatLength { |
| return nil, errors.New("conflicting format length") |
| } |
| if len(js.formatTail) == 0 { |
| js.formatTail = formatTail |
| } else if bytes.Compare(js.formatTail, formatTail) != 0 { |
| return nil, errors.New("conflicting format tail") |
| } |
| |
| header := jsHeader{ |
| Algorithm: signatureBlock.Header.Algorithm, |
| Chain: signatureBlock.Header.Chain, |
| } |
| if signatureBlock.Header.JWK != nil { |
| publicKey, err := UnmarshalPublicKeyJWK([]byte(signatureBlock.Header.JWK)) |
| if err != nil { |
| return nil, fmt.Errorf("error unmarshalling public key: %s", err) |
| } |
| header.JWK = publicKey |
| } |
| js.signatures[i] = jsSignature{ |
| Header: header, |
| Signature: signatureBlock.Signature, |
| Protected: signatureBlock.Protected, |
| } |
| } |
| if js.formatLength > len(content) { |
| return nil, errors.New("invalid format length") |
| } |
| formatted := make([]byte, js.formatLength+len(js.formatTail)) |
| copy(formatted, content[:js.formatLength]) |
| copy(formatted[js.formatLength:], js.formatTail) |
| js.indent = detectJSONIndent(formatted) |
| js.payload = joseBase64UrlEncode(formatted) |
| |
| return js, nil |
| } |
| |
| // PrettySignature formats a json signature into an easy to read |
| // single json serialized object. |
| func (js *JSONSignature) PrettySignature(signatureKey string) ([]byte, error) { |
| if len(js.signatures) == 0 { |
| return nil, errors.New("no signatures") |
| } |
| payload, err := joseBase64UrlDecode(js.payload) |
| if err != nil { |
| return nil, err |
| } |
| payload = payload[:js.formatLength] |
| |
| sort.Sort(jsSignaturesSorted(js.signatures)) |
| |
| var marshalled []byte |
| var marshallErr error |
| if js.indent != "" { |
| marshalled, marshallErr = json.MarshalIndent(js.signatures, js.indent, js.indent) |
| } else { |
| marshalled, marshallErr = json.Marshal(js.signatures) |
| } |
| if marshallErr != nil { |
| return nil, marshallErr |
| } |
| |
| buf := bytes.NewBuffer(make([]byte, 0, len(payload)+len(marshalled)+34)) |
| buf.Write(payload) |
| buf.WriteByte(',') |
| if js.indent != "" { |
| buf.WriteByte('\n') |
| buf.WriteString(js.indent) |
| buf.WriteByte('"') |
| buf.WriteString(signatureKey) |
| buf.WriteString("\": ") |
| buf.Write(marshalled) |
| buf.WriteByte('\n') |
| } else { |
| buf.WriteByte('"') |
| buf.WriteString(signatureKey) |
| buf.WriteString("\":") |
| buf.Write(marshalled) |
| } |
| buf.WriteByte('}') |
| |
| return buf.Bytes(), nil |
| } |
| |
| // Signatures provides the signatures on this JWS as opaque blobs, sorted by |
| // keyID. These blobs can be stored and reassembled with payloads. Internally, |
| // they are simply marshaled json web signatures but implementations should |
| // not rely on this. |
| func (js *JSONSignature) Signatures() ([][]byte, error) { |
| sort.Sort(jsSignaturesSorted(js.signatures)) |
| |
| var sb [][]byte |
| for _, jsig := range js.signatures { |
| p, err := json.Marshal(jsig) |
| if err != nil { |
| return nil, err |
| } |
| |
| sb = append(sb, p) |
| } |
| |
| return sb, nil |
| } |
| |
| // Merge combines the signatures from one or more other signatures into the |
| // method receiver. If the payloads differ for any argument, an error will be |
| // returned and the receiver will not be modified. |
| func (js *JSONSignature) Merge(others ...*JSONSignature) error { |
| merged := js.signatures |
| for _, other := range others { |
| if js.payload != other.payload { |
| return fmt.Errorf("payloads differ from merge target") |
| } |
| merged = append(merged, other.signatures...) |
| } |
| |
| js.signatures = merged |
| return nil |
| } |