| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,42 +1,33 @@ | ||
| # IVXV Internet voting framework | ||
| """Logging helper for microservice management.""" | ||
|
|
||
| import logging | ||
|
|
||
| # create logger | ||
| log = logging.getLogger('.'.join(__name__.split('.')[:-1])) | ||
|
|
||
|
|
||
| class ServiceLogger(logging.LoggerAdapter): | ||
| """Logger adapter for service object. | ||
| Include service ID for logged messages. | ||
| Include message level for levels other than INFO. | ||
| """ | ||
|
|
||
| def process(self, msg, kwargs): | ||
| """ | ||
| Process the logging message and keyword arguments passed in to | ||
| a logging call to insert contextual information. | ||
| """ | ||
| return f"SERVICE {self.extra['service_id']}: {msg}", kwargs | ||
|
|
||
| def log(self, level, msg, *args, **kwargs): | ||
| """ | ||
| Delegate a log call to the underlying logger, after adding | ||
| contextual information from this adapter instance. | ||
| """ | ||
| if self.isEnabledFor(level): | ||
| if level != logging.INFO: | ||
| msg = f"{logging.getLevelName(level).upper()}: {msg}" | ||
| msg, kwargs = self.process(msg, kwargs) | ||
| self.logger.log(level, msg, *args, **kwargs) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| ### collector/cmd/verifier | ||
|
|
||
| Can verify documents that are signed with: | ||
|
|
||
| - id-card | ||
| - mobile-id | ||
| - smart-id | ||
|
|
||
| #### If and only if you have the following trust.yaml configuration: | ||
| ``` | ||
| # Usaldusjuure seadistus YAML-struktuurina | ||
| container: | ||
| bdoc: | ||
| bdocsize: 104857600 # 100 MiB | ||
| filesize: 104857600 # 100 MiB | ||
| roots: | ||
| - !container devel_root.crt | ||
| - !container sk_test_root.crt | ||
| - !container EE-GovCA2018.crt | ||
| - !container EE_Certification_Centre_Root_CA.crt | ||
| intermediates: | ||
| - !container devel_intermediate.crt | ||
| - !container sk_test_intermediate.crt | ||
| - !container ESTEID-SK_2015.crt | ||
| - !container esteid2018.crt | ||
| - !container EID-SK_2016.pem.crt | ||
| profile: TS | ||
| ocsp: | ||
| responders: | ||
| - !container sk_test_ocsp.crt | ||
| - !container SK_OCSP_RESPONDER_2011.crt | ||
| - !container EID-SK_2016_OCSP_RESPONDER_2018.pem.cer | ||
| tsp: | ||
| signers: | ||
| - !container sk_test_tsa.crt | ||
| - !container SK_TIMESTAMPING_AUTHORITY_2019.crt | ||
| - !container SK_TIMESTAMPING_AUTHORITY_2020.crt | ||
| - !container SK_TIMESTAMPING_AUTHORITY_2021.crt | ||
| - !container SK_TIMESTAMPING_AUTHORITY_2022.crt | ||
| delaytime: 10 | ||
| authorizations: | ||
| - PEREKONNANIMI,NIMI,ISIKUKOOD | ||
| ``` | ||
|
|
||
| #### P.S Don't forget to include all these listed certificates into container |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,35 +1,34 @@ | ||
| -----BEGIN CERTIFICATE----- | ||
| MIIF8TCCA9mgAwIBAgIQUsz4AdR7FcpcrHiyXaldkjANBgkqhkiG9w0BAQsFADBr | ||
| MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 | ||
| czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHzAdBgNVBAMMFlRFU1Qgb2YgRVNU | ||
| RUlELVNLIDIwMTUwHhcNMTkwNDA5MTA0OTIyWhcNMzAxMjExMjE1OTU5WjCBpTEL | ||
| MAkGA1UEBhMCRUUxPTA7BgNVBAMMNE/igJlDT05ORcW9LcWgVVNMSUsgVEVTVE5V | ||
| TUJFUixNQVJZIMOETk4sNjAwMDEwMTg4MDAxJzAlBgNVBAQMHk/igJlDT05ORcW9 | ||
| LcWgVVNMSUsgVEVTVE5VTUJFUjESMBAGA1UEKgwJTUFSWSDDhE5OMRowGAYDVQQF | ||
| ExFQTk9FRS02MDAwMTAxODgwMDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEG8 | ||
| MxwPLh5qmCfkkAPMw+8nKf4cqDETMoWiFiVOGu3cdI61ARLdRQUfa9wpzFDQGtmK | ||
| uScHrLE25ZPZWEozK72jggIfMIICGzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIG | ||
| QDB1BgNVHSAEbjBsMF8GCisGAQQBzh8DAQMwUTAeBggrBgEFBQcCAjASDBBPbmx5 | ||
| IGZvciBURVNUSU5HMC8GCCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5lZS9yZXBv | ||
| c2l0b29yaXVtL0NQUzAJBgcEAIvsQAECMB0GA1UdDgQWBBRYUZMh7LjBd2OpIXrj | ||
| 0YnUK1hPJzCBigYIKwYBBQUHAQMEfjB8MAgGBgQAjkYBATAIBgYEAI5GAQQwUQYG | ||
| BACORgEFMEcwRRY/aHR0cHM6Ly9zay5lZS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlv | ||
| bnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjATBgYEAI5GAQYwCQYHBACO | ||
| RgEGATAfBgNVHSMEGDAWgBRJwPJEOWXVm0Y7DThgg7HWLSiGpjCBgwYIKwYBBQUH | ||
| AQEEdzB1MCwGCCsGAQUFBzABhiBodHRwOi8vYWlhLmRlbW8uc2suZWUvZXN0ZWlk | ||
| MjAxNTBFBggrBgEFBQcwAoY5aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVT | ||
| VF9vZl9FU1RFSUQtU0tfMjAxNS5kZXIuY3J0MDQGA1UdHwQtMCswKaAnoCWGI2h0 | ||
| dHBzOi8vYy5zay5lZS90ZXN0X2VzdGVpZDIwMTUuY3JsMA0GCSqGSIb3DQEBCwUA | ||
| A4ICAQAWPebd0D8hssTj7Cdzp6zCFtHsZjmcgn21hLzsVDYqSde+/M7aLJ9WrCNl | ||
| SvjldScWRXBBhwH7SmePxVvmi061fmlb2Mg22XCOqWOp+Eyt9LtTtHlSi21v5VNh | ||
| nVX0RRPlCGseXHAyYjLtIGx744LCsZ/nblYtfAYrh2fJnYBOJddiUctKfb96M/sZ | ||
| NFlfAXbejBpoi0a+wXmL8fTY/PNAWT2UNhsUWA3XgQpsca0TBb6m4rVc9VfnjH0v | ||
| V9gaboH8jJL0M5bfPa4oE676Uw4YhtbRp2gXFdMKjb/5KpdAdfb5EhGSk8+rWZnW | ||
| BgfsRfHw8YnrLO3sOhPYWlXdIkJ4TPsxK/StJVIlIye5UBESxR1J4wZ0iI4wvwQU | ||
| TQ7xuIS89XgjlLm9/R7Qy86GN4lj6J0lD89dnFckduN/Hk5vMA+sGvPtIsV9q/fi | ||
| Wl6+SCYMmH6D+FpVpxYhq/VQoabwSWlhsgnjE+RddP6H3pWdICwJ6r7Iyx48SUc4 | ||
| j8/lh6dxg6NR3TPQSyPQg1bJjaNO3L79pgxevX3LFOJ0eLhLmY2gvT5MvjkOjULc | ||
| XspKSjyNqyg8uJgT33SSBjArppWKMHpxybsaiY6X5jwdyO9qV32We94snptiqOrZ | ||
| aJ/LJ0RgOE7t7yBlDCdaUHohuUGB8+UD6efOBnP48L+FQz0D+w== | ||
| -----END CERTIFICATE----- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| package smartid | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/rand" | ||
| "crypto/x509" | ||
| ) | ||
|
|
||
| // Authenticate starts a Smart-ID authentication session. | ||
| func (c *Client) Authenticate(ctx context.Context, identifer string) ( | ||
| sesscode string, challengeRnd []byte, challenge []byte, err error) { | ||
|
|
||
| // Generate random authentication challenge to sign. Although we could | ||
| // use the challenge directly, we pass it through the hash function to | ||
| // simplify VerifyAuthenticationSignature which requires the pre-image | ||
| // of the signed data. | ||
| challengeRnd = make([]byte, c.authHashFunction.Size()) | ||
| if _, err = rand.Read(challengeRnd); err != nil { | ||
| err = GenerateAuthenticationChallengeError{Err: err} | ||
| return | ||
| } | ||
| d := c.authHashFunction.New() | ||
| d.Write(challengeRnd) | ||
| challenge = d.Sum(nil) | ||
|
|
||
| hashType := hashFunctionNames[c.authHashFunction] | ||
|
|
||
| sesscode, err = c.startSession(ctx, sessAuth, convertToETSI(identifer), challenge, hashType) | ||
| if err != nil { | ||
| err = AuthenticateError{Err: err} | ||
| return | ||
| } | ||
|
|
||
| return | ||
| } | ||
|
|
||
| // GetAuthenticateStatus queries the status of a Smart-ID authentication | ||
| // session. If err is nil and signature is empty, then the transaction is still | ||
| // outstanding. If err is nil and signature is non-nil, then the user is | ||
| // authenticated, although callers should use VerifyAuthenticationSignature to | ||
| // double-check. | ||
| func (c *Client) GetAuthenticateStatus(ctx context.Context, sesscode string) ( | ||
| cert *x509.Certificate, algorithm string, signature []byte, err error) { | ||
|
|
||
| var certDER []byte | ||
| _, algorithm, signature, certDER, err = c.getSessionStatus(ctx, sesscode) | ||
| if err != nil { | ||
| err = GetAuthenticateStatusError{Err: err} | ||
| return | ||
| } | ||
|
|
||
| if certDER != nil { | ||
| cert, err = c.parseAndVerify(ctx, certDER) | ||
| } | ||
|
|
||
| return | ||
| } | ||
|
|
||
| // parseAndVerify is a helper function to parse and verify the authentication | ||
| // certificate. | ||
| func (c *Client) parseAndVerify(ctx context.Context, certDER []byte) ( | ||
| cert *x509.Certificate, err error) { | ||
| // Parse the authentication certificate. | ||
| if cert, err = x509.ParseCertificate(certDER); err != nil { | ||
| err = ParseAuthenticationCertificateError{ | ||
| Certificate: certDER, | ||
| Err: err, | ||
| } | ||
| return | ||
| } | ||
|
|
||
| // Verify the authentication certificate and get the issuer. | ||
| opts := x509.VerifyOptions{ | ||
| Roots: c.rpool, | ||
| Intermediates: c.ipool, | ||
| KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, | ||
| } | ||
| chains, err := cert.Verify(opts) | ||
| if err != nil { | ||
| var certerr CertificateError | ||
| certerr.Err = AuthenticationCertificateVerificationError{ | ||
| Certificate: cert, | ||
| Err: err, | ||
| } | ||
| err = certerr | ||
| return | ||
| } | ||
| issuer := cert | ||
| if len(chains[0]) > 1 { // At least one chain is guaranteed. | ||
| issuer = chains[0][1] | ||
| } | ||
|
|
||
| // Check OCSP status. | ||
| status, err := c.ocsp.Check(ctx, cert, issuer, nil) | ||
| if err != nil { | ||
| err = CheckAuthenticationCertOCSPResponsError{ | ||
| Response: status, | ||
| Err: err, | ||
| } | ||
| return | ||
| } | ||
| if !status.Good { | ||
| var certerr CertificateError | ||
| certerr.Err = AuthenticationCertificateRevokedError{ | ||
| Reason: status.RevocationReason, | ||
| } | ||
| err = certerr | ||
| return | ||
| } | ||
|
|
||
| return | ||
| } | ||
|
|
||
| // VerifyAuthenticationSignature verifies the certificate signature on the | ||
| // authentication challenge. | ||
| func VerifyAuthenticationSignature(cert *x509.Certificate, algorithm string, | ||
| signed, signature []byte) (err error) { | ||
|
|
||
| sigalg, ok := signatureAlgs[algorithm] | ||
| if !ok { | ||
| return SigAlgorithmNotSupportedError{ | ||
| Algorithm: algorithm, | ||
| } | ||
| } | ||
|
|
||
| if err = cert.CheckSignature(sigalg, signed, signature); err != nil { | ||
| return VerifyAuthenticationSignatureError{Err: err} | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // convertToETSI is a helper function to make identifier to ETSI identifier. | ||
| func convertToETSI(identifier string) string { | ||
| return "PNOEE-" + identifier | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| package smartid | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/x509" | ||
| ) | ||
|
|
||
| // https://github.com/SK-EID/smart-id-documentation#2384-request-parameters | ||
| type getCertificate struct { | ||
| RelyingPartyUUID string `json:"relyingPartyUUID"` | ||
| RelyingPartyName string `json:"relyingPartyName"` | ||
| CertificateLevel string `json:"certificateLevel,omitempty"` | ||
| Nonce string `json:"nonce,omitempty"` | ||
| Capabilities []string `json:"capabilities,omitempty"` | ||
| } | ||
|
|
||
| // GetCertificateChoice starts a Smart-ID certificate choice session. | ||
| func (c *Client) GetCertificateChoice(ctx context.Context, identifier string) (sess string, err error) { | ||
|
|
||
| // We cannot use a struct literal, because gen would report it | ||
| // as a duplicate error type. | ||
| var input InputError | ||
|
|
||
| if len(identifier) == 0 { | ||
| input.Err = GetCertificateNoIDCodeError{} | ||
| err = input | ||
| } | ||
| if err != nil { | ||
| return | ||
| } | ||
|
|
||
| var resp startSessionResponse | ||
| if err = httpPost(ctx, c.url+"certificatechoice/etsi/"+convertToETSI(identifier), getCertificate{ | ||
| RelyingPartyUUID: c.conf.RelyingPartyUUID, | ||
| RelyingPartyName: c.conf.RelyingPartyName, | ||
| CertificateLevel: c.conf.CertificateLevel, | ||
| }, &resp); err != nil { | ||
| return "", GetMobileCertificateError{Err: err} | ||
| } | ||
|
|
||
| return resp.SessionID, nil | ||
| } | ||
|
|
||
| // GetCertificateChoiceStatus queries for a Smart-ID certificate choice session. | ||
| func (c *Client) GetCertificateChoiceStatus(ctx context.Context, sesscode string) ( | ||
| documentno string, cert *x509.Certificate, err error) { | ||
|
|
||
| var certDER []byte | ||
| documentno, _, _, certDER, err = c.getSessionStatus(ctx, sesscode) | ||
| if err != nil { | ||
| err = GetMobileCertificateStatusError{Err: err} | ||
| return | ||
| } | ||
|
|
||
| if certDER != nil { | ||
| cert, err = c.parseAndVerify(ctx, certDER) | ||
| } | ||
|
|
||
| return | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| package smartid | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "io/ioutil" | ||
| "net/http" | ||
| "net/http/httputil" | ||
|
|
||
| "ivxv.ee/log" | ||
| "ivxv.ee/safereader" | ||
| ) | ||
|
|
||
| const maxResponseSize = 10240 // 10 KiB. | ||
|
|
||
| // https://github.com/SK-EID/smart-id-documentation#2383-error-conditions | ||
| type errorResponse struct { | ||
| SessionID string `json:"sessionID"` | ||
| } | ||
|
|
||
| func httpGet(ctx context.Context, url string, resp interface{}) error { | ||
| httpReq, err := http.NewRequest(http.MethodGet, url, nil) | ||
| if err != nil { | ||
| return CreateHTTPGetRequestError{URL: url, Err: err} | ||
| } | ||
| httpReq = httpReq.WithContext(ctx) | ||
|
|
||
| return httpDo(ctx, "", httpReq, resp) | ||
| } | ||
|
|
||
| func httpPost(ctx context.Context, url string, req interface{}, resp interface{}) error { | ||
| jsonReq, err := json.Marshal(req) | ||
| if err != nil { | ||
| return MarshalJSONRequestError{Err: err} | ||
| } | ||
|
|
||
| httpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonReq)) | ||
| if err != nil { | ||
| return CreateHTTPPostRequestError{URL: url, Err: err} | ||
| } | ||
| httpReq = httpReq.WithContext(ctx) | ||
|
|
||
| httpReq.Header.Set("Content-Type", "application/json") | ||
|
|
||
| return httpDo(ctx, fmt.Sprintf("%T", req), httpReq, resp) | ||
| } | ||
|
|
||
| func httpDo(ctx context.Context, tag string, httpReq *http.Request, resp interface{}) error { | ||
| reqDump, err := httputil.DumpRequestOut(httpReq, true) | ||
| if err != nil { | ||
| return DumpHTTPRequestError{Err: err} | ||
| } | ||
| log.Debug(ctx, HTTPRequest{Request: string(reqDump)}) | ||
|
|
||
| log.Log(ctx, SendingRequest{URL: httpReq.URL, Method: httpReq.Method, BodyType: tag}) | ||
| httpResp, err := http.DefaultClient.Do(httpReq) | ||
| if err != nil { | ||
| return log.Alert(SendRequestError{Err: err}) | ||
| } | ||
| defer func() { | ||
| if cerr := httpResp.Body.Close(); cerr != nil && err == nil { | ||
| err = ResponseBodyCloseError{Err: cerr} | ||
| } | ||
| }() | ||
| log.Log(ctx, ReceivedResponse{}) | ||
|
|
||
| respDump, err := httputil.DumpResponse(httpResp, false) | ||
| if err != nil { | ||
| return DumpHTTPResponseError{Err: err} | ||
| } | ||
| log.Debug(ctx, HTTPResponse{Response: string(respDump)}) | ||
|
|
||
| // Does encoding/json.Unmarshal retain any references to the | ||
| // original byte slice in the unmarshaled structure? If not, then | ||
| // instead of allocating a new byte slice here we could reuse pooled | ||
| // buffers for temporarily storing the JSON between reading and | ||
| // decoding. | ||
| body, err := ioutil.ReadAll(safereader.New(httpResp.Body, maxResponseSize)) | ||
| if err != nil { | ||
| return ReadHTTPResponseBodyError{Err: err} | ||
| } | ||
| log.Debug(ctx, HTTPResponseBody{Body: string(body)}) | ||
|
|
||
| if httpResp.StatusCode != http.StatusOK { | ||
| if len(body) > 0 { | ||
| var jsonErr errorResponse | ||
| if err = json.Unmarshal(body, &jsonErr); err != nil { | ||
| return UnmarshalJSONErrorResponseError{Err: err} | ||
| } | ||
|
|
||
| err = ErrorResponseError{ | ||
| HTTPStatus: httpResp.Status, | ||
| SessionID: jsonErr.SessionID, | ||
| } | ||
| return err | ||
|
|
||
| } | ||
| return HTTPStatusError{Status: httpResp.Status} | ||
| } | ||
|
|
||
| if err = json.Unmarshal(body, &resp); err != nil { | ||
| return UnmarshalJSONResponseError{Err: err} | ||
| } | ||
| return nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| package smartid | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto" | ||
| "crypto/x509" | ||
| "fmt" | ||
| ) | ||
|
|
||
| type sessType string | ||
|
|
||
| const ( | ||
| sessAuth sessType = "authentication/etsi" | ||
| sessSign sessType = "signature/document" | ||
| QSCD string = "QSCD" | ||
| QUALIFIED string = "QUALIFIED" | ||
| ) | ||
|
|
||
| var ( | ||
| // hashFunctionNames is map from hash algorithm to it's name for Smart-ID | ||
| // REST API. | ||
| hashFunctionNames = map[crypto.Hash]string{ | ||
| crypto.SHA256: "SHA256", | ||
| crypto.SHA384: "SHA384", | ||
| crypto.SHA512: "SHA512", | ||
| } | ||
|
|
||
| // signatureAlgs is map from signature algorithm name in Smart-ID REST API | ||
| // to algorithm type. | ||
| signatureAlgs = map[string]x509.SignatureAlgorithm{ | ||
| "sha256WithRSAEncryption": x509.SHA256WithRSA, | ||
| "sha384WithRSAEncryption": x509.SHA384WithRSA, | ||
| "sha512WithRSAEncryption": x509.SHA512WithRSA, | ||
| } | ||
| ) | ||
|
|
||
| // https://github.com/SK-EID/smart-id-documentation#2394-request-parameters | ||
| type startSessionRequest struct { | ||
| RelyingPartyUUID string `json:"relyingPartyUUID"` | ||
| RelyingPartyName string `json:"relyingPartyName"` | ||
| CertificateLevel string `json:"certificateLevel,omitempty"` | ||
| Hash []byte `json:"hash"` | ||
| HashType string `json:"hashType"` | ||
| AllowedInteractionsOrder []allowedInteractionsOrder `json:"allowedInteractionsOrder"` | ||
| Nonce string `json:"nonce,omitempty"` | ||
| RequestProperties []byte `json:"requestProperties,omitempty"` | ||
| Capabilities []string `json:"capabilities,omitempty"` | ||
| } | ||
|
|
||
| type allowedInteractionsOrder struct { | ||
| Type string `json:"type"` | ||
| DisplayText60 string `json:"displayText60,omitempty"` | ||
| DisplayText200 string `json:"displayText200,omitempty"` | ||
| } | ||
|
|
||
| // https://github.com/SK-EID/smart-id-documentation#2395-example-response | ||
| type startSessionResponse struct { | ||
| SessionID string `json:"sessionID"` | ||
| } | ||
|
|
||
| // startSession is helper function to start either authentication or signing | ||
| // dialog with the user. | ||
| func (c *Client) startSession(ctx context.Context, t sessType, identifier string, | ||
| hash []byte, hashType string) (sesscode string, err error) { | ||
|
|
||
| // We cannot use a struct literal, because gen would report it | ||
| // as a duplicate error type. | ||
| var input InputError | ||
| switch { | ||
|
|
||
| case len(hash) == 0: | ||
| input.Err = StartSessionNoHashError{} | ||
| err = input | ||
| case len(hashType) == 0: | ||
| input.Err = StartSessionNoHashTypeError{} | ||
| err = input | ||
| } | ||
| if err != nil { | ||
| return | ||
| } | ||
|
|
||
| interactionsOrder := c.conf.SignInteractionsOrder | ||
| certLevel := c.conf.CertificateLevel | ||
| if t == sessAuth { | ||
| interactionsOrder = c.conf.AuthInteractionsOrder | ||
| if certLevel == QSCD { | ||
| certLevel = QUALIFIED | ||
| } | ||
| } | ||
|
|
||
| var resp startSessionResponse | ||
| if err = httpPost(ctx, c.url+string(t)+"/"+identifier, startSessionRequest{ | ||
| RelyingPartyUUID: c.conf.RelyingPartyUUID, | ||
| RelyingPartyName: c.conf.RelyingPartyName, | ||
| Hash: hash, | ||
| HashType: hashType, | ||
| CertificateLevel: certLevel, | ||
| AllowedInteractionsOrder: interactionsOrder, | ||
| }, &resp); err != nil { | ||
| err = StartSessionError{Err: err} | ||
| return | ||
| } | ||
| sesscode = resp.SessionID | ||
|
|
||
| return | ||
| } | ||
|
|
||
| // https://github.com/SK-EID/smart-id-documentation#23114-response-structure | ||
| type sessionStatusResponse struct { | ||
| State string `json:"state"` | ||
| Result resultResponse `json:"result"` | ||
| Signature signatureResponse `json:"signature"` | ||
| Cert certResponse `json:"cert"` | ||
| IgnoredProperties []string `json:"ignoredProperties"` | ||
| InteractionFlowUsed string `json:"interactionFlowUsed"` | ||
| DeviceIPAddress string `json:"deviceIpAddress"` | ||
| } | ||
|
|
||
| type resultResponse struct { | ||
| EndResult string `json:"endResult"` | ||
| DocumentNumber string `json:"documentNumber"` | ||
| } | ||
|
|
||
| type certResponse struct { | ||
| Value []byte `json:"value"` | ||
| CertificateLevel string `json:"certificateLevel"` | ||
| } | ||
|
|
||
| type signatureResponse struct { | ||
| Value []byte `json:"value"` | ||
| Algorithm string `json:"algorithm"` | ||
| } | ||
|
|
||
| // getSessionStatus is helper function to get the status of either | ||
| // authentication or signing dialog with the user. | ||
| func (c *Client) getSessionStatus(ctx context.Context, sesscode string) ( | ||
| documentNo string, algorithm string, signature []byte, certDER []byte, err error) { | ||
|
|
||
| var resp sessionStatusResponse | ||
| url := fmt.Sprintf("%ssession/%s?timeoutMs=%d", c.url, sesscode, c.conf.StatusTimeoutMS) | ||
| if err = httpGet(ctx, url, &resp); err != nil { | ||
| return "", "", nil, nil, GetSessionStatusError{Err: err} | ||
| } | ||
|
|
||
| switch resp.State { | ||
| case "RUNNING": | ||
| return | ||
| case "COMPLETE": | ||
| default: | ||
| var status StatusError | ||
| status.Err = UnexpectedSessionStateError{State: resp.State} | ||
| return "", "", nil, nil, status | ||
| } | ||
| switch resp.Result.EndResult { | ||
| case "OK": | ||
| return resp.Result.DocumentNumber, resp.Signature.Algorithm, resp.Signature.Value, resp.Cert.Value, nil | ||
| case "TIMEOUT": | ||
| var expired ExpiredError | ||
| return "", "", nil, nil, expired | ||
| case "DOCUMENT_UNUSABLE": | ||
| var account AccountError | ||
| return "", "", nil, nil, account | ||
| case "REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP": | ||
| var account AccountError | ||
| return "", "", nil, nil, account | ||
| case "WRONG_VC": | ||
| var verification VerificationError | ||
| return "", "", nil, nil, verification | ||
| case "USER_REFUSED": | ||
| var canceled CanceledError | ||
| return "", "", nil, nil, canceled | ||
| case "USER_REFUSED_CERT_CHOICE": | ||
| var canceled CanceledError | ||
| return "", "", nil, nil, canceled | ||
| case "USER_REFUSED_DISPLAYTEXTANDPIN": | ||
| var canceled CanceledError | ||
| return "", "", nil, nil, canceled | ||
| case "USER_REFUSED_VC_CHOICE": | ||
| var canceled CanceledError | ||
| return "", "", nil, nil, canceled | ||
| case "USER_REFUSED_CONFIRMATIONMESSAGE": | ||
| var canceled CanceledError | ||
| return "", "", nil, nil, canceled | ||
| case "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE": | ||
| var canceled CanceledError | ||
| return "", "", nil, nil, canceled | ||
| default: | ||
| var status StatusError | ||
| status.Err = UnexpectedSessionResultError{Result: resp.Result} | ||
| return "", "", nil, nil, status | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package smartid | ||
|
|
||
| import ( | ||
| "context" | ||
| ) | ||
|
|
||
| // SignHash starts a Smart-ID signing session to sigh hash. | ||
| func (c *Client) SignHash(ctx context.Context, documentno string, hash []byte, | ||
| hashType string) (sesscode string, err error) { | ||
|
|
||
| sesscode, err = c.startSession(ctx, sessSign, documentno, hash, hashType) | ||
| if err != nil { | ||
| err = SignHashError{Err: err} | ||
| return | ||
| } | ||
|
|
||
| return | ||
| } | ||
|
|
||
| // GetSignHashStatus queries the status of a Smart-ID signing session. | ||
| // If err is nil and signature is empty, then the transaction is still | ||
| // outstanding. | ||
| func (c *Client) GetSignHashStatus(ctx context.Context, sesscode string) ( | ||
| algorithm string, signature []byte, err error) { | ||
|
|
||
| _, algorithm, signature, _, err = c.getSessionStatus(ctx, sesscode) | ||
| if err != nil { | ||
| err = GetSignHashStatusError{Err: err} | ||
| return | ||
| } | ||
| return | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| /* | ||
| Package smartid provides client for the Smart-ID REST service. | ||
|
|
||
| https://github.com/SK-EID/smart-id-documentation | ||
| */ | ||
| package smartid | ||
|
|
||
| import ( | ||
| "crypto" | ||
| "crypto/x509" | ||
| "strings" | ||
|
|
||
| "ivxv.ee/cryptoutil" | ||
| "ivxv.ee/ocsp" | ||
| ) | ||
|
|
||
| var ( | ||
| // InputError wraps errors which are caused by bad input to smartid functions. | ||
| _ = InputError{Err: nil} | ||
|
|
||
| // VerificationError is the error returned if the voter 3 different code | ||
| // was displayed in app and selected wrong code. | ||
| _ = VerificationError{} | ||
|
|
||
| // AccountError is the error returned that are caused by user account configuration. | ||
| _ = AccountError{} | ||
|
|
||
| // CanceledError is the error returned if the voter canceled the | ||
| // operation. | ||
| _ = CanceledError{} | ||
|
|
||
| // ExpiredError is the error returned if the session expired | ||
| // before the voter did any action. | ||
| _ = ExpiredError{} | ||
|
|
||
| // CertificateError wraps errors which are caused by errors with the | ||
| // voter's certificate (revoked, suspended, not activated, etc). | ||
| _ = CertificateError{Err: nil} | ||
|
|
||
| // StatusError wraps errors which are caused by an unexpected session | ||
| // status: this is a catch-all for other types of Smart-ID problems. | ||
| _ = StatusError{Err: nil} | ||
|
|
||
| // allowedAuthHashFunctions is list of hash functions allowed for | ||
| // authentication. Since the hash function is determined by it's hash | ||
| // size (Conf.AuthChallengeSize), the functions should have hash sizes | ||
| // unique in the list. If Conf.AuthChallengeSize is zero, then the | ||
| // first one from this list is used. | ||
| allowedAuthHashFunctions = []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512} | ||
| ) | ||
|
|
||
| // Conf contains the configurable options for the Smart-ID REST API client. It | ||
| // only contains serialized values such that it can easily be unmarshaled from | ||
| // a file. | ||
| type Conf struct { | ||
| URL string // URL of Smart-ID REST API. | ||
| RelyingPartyUUID string // The UUID of the relying party, i.e service consumer. | ||
| RelyingPartyName string // The name of the relying party, i.e service consumer. | ||
|
|
||
| CertificateLevel string // Certificate level for requests | ||
| AuthInteractionsOrder []allowedInteractionsOrder // Interactions to display during authentication. | ||
| SignInteractionsOrder []allowedInteractionsOrder // Interactions to display during signing. | ||
|
|
||
| AuthChallengeSize int64 // The authentication challenge size. | ||
| StatusTimeoutMS int64 // The long-polling timeout for authentication/signing status request. | ||
|
|
||
| Roots []string // PEM-encoded authentication certificate verification roots. | ||
| Intermediates []string // PEM-encoded authentication certificate verification intermediates. | ||
| OCSP ocsp.Conf // OCSP configuration for checking authentication certificate revocation. | ||
| } | ||
|
|
||
| // Client implements Smart-ID REST API authentication and signing. | ||
| type Client struct { | ||
| conf Conf | ||
| rpool *x509.CertPool | ||
| ipool *x509.CertPool | ||
| ocsp *ocsp.Client | ||
| // authHashFunction is the hash function to use for authentication. | ||
| // Determined by Conf.AuthChallengeSize. | ||
| authHashFunction crypto.Hash | ||
| // url is the same as 'conf.URL', but guaranteed to end with slash. | ||
| url string | ||
| } | ||
|
|
||
| // New returns a new Smart-ID REST API client with the provided configuration. | ||
| func New(conf *Conf) (c *Client, err error) { | ||
| if len(conf.Roots) == 0 { | ||
| return nil, UnconfiguredRootsError{} | ||
| } | ||
|
|
||
| c = &Client{conf: *conf} // Save a copy of conf so it cannot be changed. | ||
| if c.rpool, err = cryptoutil.PEMCertificatePool(c.conf.Roots...); err != nil { | ||
| return nil, RootsParsingError{Err: err} | ||
| } | ||
| if c.ipool, err = cryptoutil.PEMCertificatePool(c.conf.Intermediates...); err != nil { | ||
| return nil, IntermediatesParsingError{Err: err} | ||
| } | ||
| if c.ocsp, err = ocsp.New(&c.conf.OCSP); err != nil { | ||
| return nil, OCSPClientError{Err: err} | ||
| } | ||
| if c.authHashFunction, err = findAuthHashFunction(c.conf.AuthChallengeSize); err != nil { | ||
| return nil, err | ||
| } | ||
| c.url = conf.URL | ||
| if !strings.HasSuffix(c.url, "/") { | ||
| c.url += "/" | ||
| } | ||
| return | ||
| } | ||
|
|
||
| // findAuthHashFunction is helper function to find allowed authentication hash | ||
| // algorithm by it's hash size. If the size is not configured, the first value | ||
| // in the allowedAuthHashFunctions is used. | ||
| func findAuthHashFunction(size int64) (crypto.Hash, error) { | ||
| if size == 0 { | ||
| return allowedAuthHashFunctions[0], nil | ||
| } | ||
| var sizes []int | ||
| for _, hf := range allowedAuthHashFunctions { | ||
| if hf.Size() == int(size) { | ||
| return hf, nil | ||
| } | ||
| sizes = append(sizes, hf.Size()) | ||
| } | ||
| return 0, AuthChallengeSizeError{Size: size, AllowedSizes: sizes} | ||
| } |